diff --git a/.typos.toml b/.typos.toml index 038573c4..9b8271d7 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,7 +4,6 @@ PUNICODE = "PUNICODE" [files] extend-exclude = [ - "**/snap-tests/**/snap.txt", "crates/fspy_detours_sys/detours", "crates/fspy_detours_sys/src/generated_bindings.rs", ] diff --git a/Cargo.lock b/Cargo.lock index d2915bc5..64db324d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,12 +50,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.21" @@ -118,25 +112,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "ascii-canvas" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" -dependencies = [ - "term", -] - -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "assert2" version = "0.3.16" @@ -166,201 +141,14 @@ version = "9.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59051ec02907378a67b0ba1b8631121f5388c8dbbb3cec8c749d8f93c2c3c211" -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.2", - "slab", - "windows-sys 0.61.1", -] - -[[package]] -name = "async-lock" -version = "3.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-object-pool" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c" -dependencies = [ - "async-std", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel 2.5.0", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.1", - "futures-lite", - "rustix 1.1.2", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.2", - "signal-hook-registry", - "slab", - "windows-sys 0.61.1", -] - -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "attohttpc" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "base64 0.22.1", - "http 1.3.1", + "base64", + "http", "log", "native-tls", "url", @@ -372,17 +160,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backon" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -398,29 +175,12 @@ dependencies = [ "windows-link", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "basic-cookies" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" -dependencies = [ - "lalrpop", - "lalrpop-util", - "regex", -] - [[package]] name = "bincode" version = "2.0.1" @@ -461,21 +221,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -497,19 +242,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "brush-parser" version = "0.2.20" @@ -594,12 +326,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "castaway" version = "0.2.4" @@ -646,33 +372,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -684,46 +383,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "4.5.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - [[package]] name = "color-eyre" version = "0.6.5" @@ -786,15 +445,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "const_format" version = "0.2.34" @@ -831,15 +481,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -874,39 +515,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" -dependencies = [ - "cast", - "itertools 0.13.0", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -976,12 +584,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.6" @@ -1017,32 +619,16 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctor" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" -dependencies = [ - "ctor-proc-macro 0.0.6", - "dtor", -] - [[package]] name = "ctor" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c9b8bdf64ee849747c1b12eb861d21aa47fa161564f48332f1afe2373bf899" dependencies = [ - "ctor-proc-macro 0.0.7", + "ctor-proc-macro", "dtor", ] -[[package]] -name = "ctor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" - [[package]] name = "ctor-proc-macro" version = "0.0.7" @@ -1113,7 +699,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case 0.7.1", + "convert_case", "proc-macro2", "quote", "syn 2.0.106", @@ -1166,16 +752,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.5.0" @@ -1184,21 +760,10 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.1", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1252,15 +817,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" -[[package]] -name = "ena" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" -dependencies = [ - "log", -] - [[package]] name = "env_filter" version = "0.1.3" @@ -1304,33 +860,6 @@ dependencies = [ "windows-sys 0.61.1", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", -] - [[package]] name = "eyre" version = "0.6.12" @@ -1388,12 +917,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fixedbitset" version = "0.5.7" @@ -1458,7 +981,7 @@ dependencies = [ "bumpalo", "const_format", "csv-async", - "ctor 0.6.0", + "ctor", "flate2", "fspy_detours_sys", "fspy_preload_unix", @@ -1507,7 +1030,7 @@ dependencies = [ "anyhow", "bincode", "bstr", - "ctor 0.6.0", + "ctor", "fspy_shared", "fspy_shared_unix", "libc", @@ -1557,7 +1080,7 @@ dependencies = [ "bincode", "bstr", "bytemuck", - "ctor 0.6.0", + "ctor", "fspy_test_utils", "os_str_bytes", "shared_memory", @@ -1573,7 +1096,7 @@ name = "fspy_shared_unix" version = "0.0.0" dependencies = [ "anyhow", - "base64 0.22.1", + "base64", "bincode", "bstr", "elf", @@ -1596,9 +1119,9 @@ dependencies = [ name = "fspy_test_utils" version = "0.0.0" dependencies = [ - "base64 0.22.1", + "base64", "bincode", - "ctor 0.6.0", + "ctor", ] [[package]] @@ -1649,19 +1172,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.31" @@ -1720,10 +1230,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1733,11 +1241,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -1752,28 +1258,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "half" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -1818,29 +1302,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" @@ -1852,181 +1313,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "httpmock" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" -dependencies = [ - "assert-json-diff", - "async-object-pool", - "async-std", - "async-trait", - "base64 0.21.7", - "basic-cookies", - "crossbeam-utils", - "form_urlencoded", - "futures-util", - "hyper 0.14.32", - "lazy_static", - "levenshtein", - "log", - "regex", - "serde", - "serde_json", - "serde_regex", - "similar", - "tokio", - "url", -] - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec 1.15.1", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.3.1", - "hyper 1.7.0", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.0", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -2173,33 +1459,17 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.106", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +] [[package]] -name = "iri-string" -version = "0.7.8" +name = "io-uring" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "memchr", - "serde", + "bitflags 2.9.4", + "cfg-if", + "libc", ] [[package]] @@ -2272,58 +1542,12 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - -[[package]] -name = "lalrpop" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.11.0", - "lalrpop-util", - "petgraph 0.6.5", - "pico-args", - "regex", - "regex-syntax", - "string_cache", - "term", - "tiny-keccak", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" -dependencies = [ - "regex-automata", -] - [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "levenshtein" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" - [[package]] name = "libc" version = "0.2.176" @@ -2411,9 +1635,6 @@ name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -dependencies = [ - "value-bag", -] [[package]] name = "lru" @@ -2424,12 +1645,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -2499,64 +1714,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "napi" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7" -dependencies = [ - "anyhow", - "bitflags 2.9.4", - "ctor 0.5.0", - "napi-build", - "napi-sys", - "nohash-hasher", - "rustc-hash", - "tokio", -] - -[[package]] -name = "napi-build" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14" - -[[package]] -name = "napi-derive" -version = "3.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0" -dependencies = [ - "convert_case 0.8.0", - "ctor 0.5.0", - "napi-derive-backend", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "napi-derive-backend" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a" -dependencies = [ - "convert_case 0.8.0", - "proc-macro2", - "quote", - "semver 1.0.27", - "syn 2.0.106", -] - -[[package]] -name = "napi-sys" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" -dependencies = [ - "libloading", -] - [[package]] name = "native-tls" version = "0.2.14" @@ -2574,12 +1731,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "nix" version = "0.23.2" @@ -2618,12 +1769,6 @@ dependencies = [ "memoffset 0.9.1", ] -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - [[package]] name = "nom" version = "7.1.3" @@ -2746,12 +1891,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "openssl" version = "0.10.74" @@ -2851,12 +1990,6 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.4" @@ -2896,12 +2029,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "peg" version = "0.8.5" @@ -2935,23 +2062,13 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap", -] - [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap", "serde", @@ -3000,12 +2117,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3018,65 +2129,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.2", - "windows-sys 0.61.1", -] - [[package]] name = "pori" version = "0.0.0" @@ -3125,12 +2183,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "prettyplease" version = "0.2.37" @@ -3163,61 +2215,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases 0.2.1", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases 0.2.1", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.52.0", -] - [[package]] name = "quote" version = "1.0.41" @@ -3342,17 +2339,6 @@ dependencies = [ "bitflags 2.9.4", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -3413,64 +2399,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "native-tls", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rusqlite" version = "0.37.0" @@ -3541,41 +2469,6 @@ dependencies = [ "windows-sys 0.61.1", ] -[[package]] -name = "rustls" -version = "0.23.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -3597,15 +2490,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.28" @@ -3621,12 +2505,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "seccompiler" version = "0.5.0" @@ -3715,22 +2593,12 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap", - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_regex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" -dependencies = [ - "regex", + "indexmap", + "itoa", + "memchr", + "ryu", "serde", + "serde_core", ] [[package]] @@ -3742,18 +2610,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_yml" version = "0.0.12" @@ -3780,42 +2636,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "serial_test" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" -dependencies = [ - "futures", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -3907,12 +2727,6 @@ dependencies = [ "libc", ] -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - [[package]] name = "siphasher" version = "1.0.1" @@ -3937,16 +2751,6 @@ version = "2.0.0-alpha.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87b96efa4bd6bdd2ff0c6615cc36fc4970cbae63cfd46ddff5cee35a1b4df570" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -3979,18 +2783,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - [[package]] name = "strsim" version = "0.11.1" @@ -4019,12 +2811,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "supports-color" version = "3.0.2" @@ -4056,15 +2842,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -4106,17 +2883,6 @@ dependencies = [ "windows-sys 0.61.1", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "test-log" version = "0.2.18" @@ -4188,15 +2954,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -4207,31 +2964,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.47.1" @@ -4247,7 +2979,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -4263,26 +2995,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -4346,51 +3058,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.41" @@ -4463,12 +3130,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "tui-term" version = "0.2.0" @@ -4535,12 +3196,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "unty" version = "0.0.4" @@ -4597,12 +3252,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" - [[package]] name = "vcpkg" version = "0.2.15" @@ -4621,61 +3270,6 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "vite-plus-benches" -version = "0.1.0" -dependencies = [ - "criterion", - "vite_path", - "vite_task", -] - -[[package]] -name = "vite-plus-cli" -version = "0.0.0" -dependencies = [ - "clap", - "crossterm 0.29.0", - "napi", - "napi-build", - "napi-derive", - "petgraph 0.8.3", - "serde", - "serde_json", - "serial_test", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "vite_error", - "vite_install", - "vite_path", - "vite_str", - "vite_task", -] - -[[package]] -name = "vite_error" -version = "0.0.0" -dependencies = [ - "anyhow", - "bincode", - "bstr", - "nix 0.30.1", - "reqwest", - "rusqlite", - "semver 1.0.27", - "serde_json", - "serde_yml", - "thiserror 2.0.17", - "tokio", - "vite_path", - "vite_str", - "vite_task", - "vite_workspace", - "wax", -] - [[package]] name = "vite_glob" version = "0.0.0" @@ -4685,37 +3279,6 @@ dependencies = [ "wax", ] -[[package]] -name = "vite_install" -version = "0.0.0" -dependencies = [ - "backon", - "directories", - "flate2", - "futures-util", - "hex", - "httpmock", - "indoc", - "nix 0.30.1", - "pathdiff", - "reqwest", - "semver 1.0.27", - "serde", - "serde_json", - "sha1", - "sha2", - "tar", - "tempfile", - "test-log", - "tokio", - "tracing", - "vite_error", - "vite_glob", - "vite_path", - "vite_str", - "vite_workspace", -] - [[package]] name = "vite_path" version = "0.1.0" @@ -4756,7 +3319,7 @@ dependencies = [ "itertools 0.14.0", "nix 0.30.1", "owo-colors", - "petgraph 0.8.3", + "petgraph", "rayon", "rusqlite", "serde", @@ -4800,7 +3363,7 @@ dependencies = [ name = "vite_workspace" version = "0.0.0" dependencies = [ - "petgraph 0.8.3", + "petgraph", "rustc-hash", "serde", "serde_json", @@ -4856,15 +3419,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4916,19 +3470,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.104" @@ -4961,19 +3502,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wax" version = "0.6.0" @@ -4989,16 +3517,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -5009,15 +3527,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "7.0.3" @@ -5416,12 +3925,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 3da9d616..4da25d44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["bench", "crates/*", "packages/cli/binding"] +members = ["crates/*"] [workspace.package] authors = ["Vite+ Authors"] @@ -37,7 +37,6 @@ anyhow = "1.0.98" assert2 = "0.3.16" assertables = "9.8.1" attohttpc = { version = "0.30.1", features = ["tls-native", "tls-native-vendored"], default-features = false } -backon = "1.3.0" base64 = "0.22.1" bincode = "2.0.1" bindgen = "0.72.1" @@ -46,12 +45,10 @@ bstr = { version = "1.12.0", default-features = false, features = ["alloc", "std bumpalo = { version = "3.17.0", features = ["allocator-api2"] } bytemuck = { version = "1.23.0", features = ["extern_crate_alloc", "must_cast"] } cc = "1.2.39" -clap = "4.5.40" color-eyre = "0.6.5" compact_str = "0.9.0" const_format = "0.2.34" constcat = "0.6.1" -criterion = { version = "0.7", features = ["html_reports"] } crossterm = { version = "0.29.0", features = ["event-stream"] } csv-async = { version = "1.3.1", features = ["tokio"] } ctor = "0.6" @@ -71,9 +68,6 @@ fspy_test_utils = { path = "crates/fspy_test_utils" } futures = "0.3.31" futures-core = "0.3.31" futures-util = "0.3.31" -hex = "0.4.3" -httpmock = "0.7" -indoc = "2.0.5" itertools = "0.14.0" libc = "0.2.172" memmap2 = "0.9.7" @@ -83,7 +77,6 @@ os_str_bytes = "7.1.1" ouroboros = "0.18.5" owo-colors = "4.1.0" passfd = { git = "https://github.com/polachok/passfd", rev = "d55881752c16aced1a49a75f9c428d38d3767213", default-features = false } -pathdiff = "0.2.3" petgraph = "0.8.2" phf = { version = "0.11.3", features = ["macros"] } portable-pty = "0.9.0" @@ -91,16 +84,12 @@ rand = "0.9.1" ratatui = "0.29.0" rayon = "1.10.0" ref-cast = "1.0.24" -reqwest = { version = "0.12", default-features = false } rusqlite = "0.37.0" rustc-hash = "2.1.1" seccompiler = { git = "https://github.com/branchseer/seccompiler", branch = "seccomp-action-raw" } -semver = "1.0.26" serde = "1.0.219" serde_json = "1.0.140" serde_yml = "0.0.12" -serial_test = "3.2.0" -sha1 = "0.10.6" sha2 = "0.10.9" shared_memory = "0.12.4" shell-escape = "0.1.5" @@ -121,12 +110,9 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter", "serde"] } tui-term = "0.2.0" twox-hash = "2.1.1" uuid = "1.18.1" -vite_error = { path = "crates/vite_error" } vite_glob = { path = "crates/vite_glob" } -vite_install = { path = "crates/vite_install" } vite_path = { path = "crates/vite_path" } vite_str = { path = "crates/vite_str" } -vite_task = { path = "crates/vite_task" } vite_workspace = { path = "crates/vite_workspace" } wax = "0.6.0" which = "7.0.3" @@ -135,10 +121,6 @@ winapi = "0.3.9" winsafe = { version = "0.0.24", features = ["kernel"] } xxhash-rust = { version = "0.8.15", features = ["const_xxh3"] } -napi = { version = "3.0.0", default-features = false, features = ["async", "error_anyhow"] } -napi-build = "2" -napi-derive = { version = "3.0.0", default-features = false, features = ["type-def", "strict"] } - [workspace.metadata.cargo-shear] # These are artifact dependencies. They are not directly `use`d in Rust code. ignored = ["fspy_preload_unix", "fspy_preload_windows", "fspy_test_bin"] diff --git a/README.md b/README.md index 48871f0a..b3b73129 100644 --- a/README.md +++ b/README.md @@ -1,24 +1 @@ -# Vite+ - -## Install internal global cli - -Add the following lines to your `~/.npmrc` file: - -``` -//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} -@voidzero-dev:registry=https://npm.pkg.github.com/ -``` - -Create a classic personal access token, following this guide: https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages#about-scopes-and-permissions-for-package-registries - -Use this token to install the global cli: - -``` -GITHUB_TOKEN= npm install -g @voidzero-dev/global -``` - -Use 1Password cli: - -``` -GITHUB_TOKEN=$(op read "op://YOUR_GITHUB_TOKEN_PATH") npm install -g @voidzero-dev/global -``` +# Vite Task diff --git a/bench/.gitignore b/bench/.gitignore deleted file mode 100644 index 5a01fe48..00000000 --- a/bench/.gitignore +++ /dev/null @@ -1 +0,0 @@ -fixtures/monorepo \ No newline at end of file diff --git a/bench/Cargo.toml b/bench/Cargo.toml deleted file mode 100644 index 21d3a24f..00000000 --- a/bench/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "vite-plus-benches" -version = "0.1.0" -edition = "2024" - -[dependencies] -vite_path = { workspace = true } -vite_task = { workspace = true } - -[dev-dependencies] -criterion = { workspace = true } - -[[bench]] -name = "workspace_load" -harness = false diff --git a/bench/benches/auto_install.sh b/bench/benches/auto_install.sh deleted file mode 100755 index b0a514b5..00000000 --- a/bench/benches/auto_install.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -# Benchmark script to compare performance with and without auto-install -# Our implementation uses VITE_TASK_EXECUTION_ENV=1 to disable nested auto-install execution - -AUTO_INSTALL_CMD="node ./packages/cli/src/bin.ts lint" - -echo "=== Performance comparison: auto-install enabled vs disabled ===" -echo "" - -# Test with auto-install enabled (default behavior) -echo "Testing with auto-install enabled..." -time ${AUTO_INSTALL_CMD} - -echo "" - -# Test with auto-install disabled (simulating nested execution) -echo "Testing with auto-install disabled (nested execution simulation)..." -time VITE_TASK_EXECUTION_ENV=1 ${AUTO_INSTALL_CMD} - -echo "" -echo "=== Running detailed benchmark with hyperfine ===" - -# Run detailed benchmark comparison -hyperfine -w 2 -r 5 -i \ - -n "auto-install-enabled" "${AUTO_INSTALL_CMD}" \ - -n "auto-install-disabled" "VITE_TASK_EXECUTION_ENV=1 ${AUTO_INSTALL_CMD}" diff --git a/bench/benches/workspace_load.rs b/bench/benches/workspace_load.rs deleted file mode 100644 index 16044951..00000000 --- a/bench/benches/workspace_load.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::{hint::black_box, path::PathBuf}; - -use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; -use vite_path::AbsolutePathBuf; -use vite_task::Workspace; - -fn bench_workspace_load(c: &mut Criterion) { - let fixture_path = AbsolutePathBuf::new(PathBuf::from(env!("CARGO_MANIFEST_DIR"))) - .unwrap() - .join("fixtures") - .join("monorepo"); - - // Basic workspace load benchmark - c.bench_function("workspace_load_1000_packages", |b| { - b.iter(|| { - let workspace = black_box(Workspace::load(fixture_path.clone(), true)) - .expect("Failed to load workspace"); - black_box(workspace); - }); - }); - - // Benchmark group for more detailed analysis - let mut group = c.benchmark_group("workspace_load_detailed"); - - group.measurement_time(std::time::Duration::from_secs(10)); - - // Benchmark just the load operation - group.bench_function("basic_load", |b| { - b.iter(|| { - let workspace = Workspace::load(black_box(fixture_path.clone()), true) - .expect("Failed to load workspace"); - black_box(workspace); - }); - }); - - // Benchmark load with cache verification - group.bench_function("load_with_cache_path", |b| { - let cache_path = fixture_path.join("node_modules/.vite/task-cache"); - b.iter(|| { - let workspace = black_box( - Workspace::load_with_cache_path( - fixture_path.clone(), - Some(cache_path.clone()), - true, - ) - .expect("Failed to load workspace"), - ); - black_box(workspace); - }); - }); - - group.finish(); - - // Benchmark different monorepo sizes - let mut size_group = c.benchmark_group("workspace_load_by_size"); - size_group.sample_size(20); - - // We only have the 100-package fixture, but we can still use it - size_group.bench_with_input(BenchmarkId::new("packages", 100), &fixture_path, |b, path| { - b.iter(|| { - let workspace = - Workspace::load(black_box(path.clone()), true).expect("Failed to load workspace"); - black_box(workspace); - }); - }); - - size_group.finish(); -} - -criterion_group!(benches, bench_workspace_load); -criterion_main!(benches); diff --git a/bench/fixtures/monorepo/package.json b/bench/fixtures/monorepo/package.json deleted file mode 100644 index 70e86a3f..00000000 --- a/bench/fixtures/monorepo/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "monorepo-benchmark", - "version": "1.0.0", - "private": true, - "workspaces": [ - "packages/*" - ], - "scripts": { - "build:all": "vite-plus run build", - "test:all": "vite-plus run test", - "lint:all": "vite-plus run lint" - }, - "devDependencies": { - "vite-plus": "*" - } -} \ No newline at end of file diff --git a/bench/fixtures/monorepo/pnpm-workspace.yaml b/bench/fixtures/monorepo/pnpm-workspace.yaml deleted file mode 100644 index 18ec407e..00000000 --- a/bench/fixtures/monorepo/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - - 'packages/*' diff --git a/bench/fixtures/monorepo/vite-plus.json b/bench/fixtures/monorepo/vite-plus.json deleted file mode 100644 index 54c4e26b..00000000 --- a/bench/fixtures/monorepo/vite-plus.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "tasks": { - "build": { - "cache": true, - "parallel": true - }, - "test": { - "cache": true, - "parallel": true - }, - "lint": { - "cache": false, - "parallel": true - } - } -} \ No newline at end of file diff --git a/bench/generate-monorepo.ts b/bench/generate-monorepo.ts deleted file mode 100644 index f56a4ae5..00000000 --- a/bench/generate-monorepo.ts +++ /dev/null @@ -1,373 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -interface Package { - name: string; - dependencies: string[]; - scripts: Record; - hasVitePlusConfig: boolean; -} - -const __dirname = path.join(fileURLToPath(import.meta.url), '..'); - -class MonorepoGenerator { - private packages: Map = new Map(); - private readonly PACKAGE_COUNT = 1000; - private readonly MAX_DEPS_PER_PACKAGE = 8; - private readonly MIN_DEPS_PER_PACKAGE = 2; - private readonly SCRIPT_NAMES = [ - 'build', - 'test', - 'lint', - 'dev', - 'start', - 'prepare', - 'compile', - ]; - private readonly CATEGORIES = ['core', 'util', 'feature', 'service', 'app']; - - constructor(private rootDir: string) {} - - private getRandomInt(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min; - } - - private getRandomElement(arr: T[]): T { - return arr[Math.floor(Math.random() * arr.length)]; - } - - private generatePackageName(index: number): string { - const category = this.getRandomElement(this.CATEGORIES); - const paddedIndex = index.toString().padStart(2, '0'); - return `${category}-${paddedIndex}`; - } - - private generateScriptCommand( - scriptName: string, - packageName: string, - ): string { - const commands = [ - `echo "Running ${scriptName} for ${packageName}"`, - `node scripts/${scriptName}.js`, - `tsc --build`, - `webpack build`, - `rollup -c`, - `esbuild src/index.js --bundle`, - `npm run pre${scriptName}`, - `node tasks/${scriptName}`, - ]; - - // Generate command with 0-3 && concatenations - const numCommands = this.getRandomInt(1, 4); - const selectedCommands: string[] = []; - - for (let i = 0; i < numCommands; i++) { - selectedCommands.push(this.getRandomElement(commands)); - } - - return selectedCommands.join(' && '); - } - - private generateScripts(packageName: string): Record { - const scripts: Record = {}; - - // Each package has 2-3 scripts - const numScripts = this.getRandomInt(2, 3); - const selectedScripts = new Set(); - - while (selectedScripts.size < numScripts) { - selectedScripts.add(this.getRandomElement(this.SCRIPT_NAMES)); - } - - for (const scriptName of selectedScripts) { - scripts[scriptName] = this.generateScriptCommand(scriptName, packageName); - } - - return scripts; - } - - private selectDependencies( - currentIndex: number, - availablePackages: string[], - ): string[] { - const numDeps = this.getRandomInt( - this.MIN_DEPS_PER_PACKAGE, - this.MAX_DEPS_PER_PACKAGE, - ); - const dependencies = new Set(); - - // Create a complex graph by selecting dependencies from different layers - // Prefer packages with lower indices (creates deeper dependency chains) - const eligiblePackages = availablePackages.filter((pkg) => { - const pkgIndex = parseInt(pkg.split('-')[1]); - return pkgIndex < currentIndex; - }); - - if (eligiblePackages.length === 0) { - return []; - } - - while ( - dependencies.size < numDeps && - dependencies.size < eligiblePackages.length - ) { - const dep = this.getRandomElement(eligiblePackages); - dependencies.add(dep); - } - - // Add some cross-category dependencies for complexity - if (Math.random() > 0.3) { - const crossCategoryDeps = availablePackages.filter((pkg) => { - const category = pkg.split('-')[0]; - return category !== currentIndex.toString().split('-')[0]; - }); - - if (crossCategoryDeps.length > 0) { - dependencies.add(this.getRandomElement(crossCategoryDeps)); - } - } - - return Array.from(dependencies); - } - - private generatePackages(): void { - // First, create all package names - const allPackageNames: string[] = []; - for (let i = 0; i < this.PACKAGE_COUNT; i++) { - allPackageNames.push(this.generatePackageName(i)); - } - - // Generate packages with dependencies - for (let i = 0; i < this.PACKAGE_COUNT; i++) { - const packageName = allPackageNames[i]; - const scripts = this.generateScripts(packageName); - - // 70% chance to have vite-plus.json config - const hasVitePlusConfig = Math.random() > 0.3; - - // Select dependencies from packages created before this one - const dependencies = i === 0 ? [] : this.selectDependencies(i, allPackageNames.slice(0, i)); - - this.packages.set(packageName, { - name: packageName, - dependencies, - scripts, - hasVitePlusConfig, - }); - } - - // Ensure complex transitive dependencies for script resolution testing - this.addTransitiveScriptDependencies(); - } - - private addTransitiveScriptDependencies(): void { - // Create specific patterns for testing transitive script dependencies - const packagesArray = Array.from(this.packages.entries()); - - for (let i = 0; i < 50; i++) { - const [nameA, pkgA] = this.getRandomElement(packagesArray); - const [nameB, pkgB] = this.getRandomElement(packagesArray); - const [nameC, pkgC] = this.getRandomElement(packagesArray); - - if (nameA !== nameB && nameB !== nameC && nameA !== nameC) { - // Setup: A depends on B, B depends on C - if (!pkgA.dependencies.includes(nameB)) { - pkgA.dependencies.push(nameB); - } - if (!pkgB.dependencies.includes(nameC)) { - pkgB.dependencies.push(nameC); - } - - // Create the scenario: A has build, B doesn't, C has build - const scriptName = this.getRandomElement(this.SCRIPT_NAMES); - pkgA.scripts[scriptName] = this.generateScriptCommand(scriptName, nameA); - delete pkgB.scripts[scriptName]; // B doesn't have the script - pkgC.scripts[scriptName] = this.generateScriptCommand(scriptName, nameC); - } - } - } - - private writePackage(pkg: Package): void { - const packageDir = path.join(this.rootDir, 'packages', pkg.name); - - // Create directory structure - fs.mkdirSync(packageDir, { recursive: true }); - fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true }); - - // Write package.json - const packageJson = { - name: `@monorepo/${pkg.name}`, - version: '1.0.0', - main: 'src/index.js', - scripts: pkg.scripts, - dependencies: pkg.dependencies.reduce((deps, dep) => { - deps[`@monorepo/${dep}`] = 'workspace:*'; - return deps; - }, {} as Record), - }; - - fs.writeFileSync( - path.join(packageDir, 'package.json'), - JSON.stringify(packageJson, null, 2), - ); - - // Write source file - const indexContent = `// ${pkg.name} module -export function ${pkg.name.replace('-', '_')}() { - console.log('Executing ${pkg.name}'); -${pkg.dependencies.map((dep) => ` require('@monorepo/${dep}');`).join('\n')} -} - -module.exports = { ${pkg.name.replace('-', '_')} }; -`; - - fs.writeFileSync(path.join(packageDir, 'src', 'index.js'), indexContent); - - // Write vite-plus.json if needed - if (pkg.hasVitePlusConfig) { - const vitePlusConfig = { - extends: '../../vite-plus.json', - tasks: { - build: { - cache: true, - env: { - NODE_ENV: 'production', - }, - }, - }, - }; - - fs.writeFileSync( - path.join(packageDir, 'vite-plus.json'), - JSON.stringify(vitePlusConfig, null, 2), - ); - } - } - - public generate(): void { - console.log('Generating monorepo structure...'); - - // Clean and create root directory - if (fs.existsSync(this.rootDir)) { - fs.rmSync(this.rootDir, { recursive: true, force: true }); - } - fs.mkdirSync(this.rootDir, { recursive: true }); - fs.mkdirSync(path.join(this.rootDir, 'packages'), { recursive: true }); - - // Generate packages - this.generatePackages(); - - // Write all packages - let count = 0; - for (const [_, pkg] of this.packages) { - this.writePackage(pkg); - count++; - if (count % 100 === 0) { - console.log(`Generated ${count} packages...`); - } - } - - // Write root package.json - const rootPackageJson = { - name: 'monorepo-benchmark', - version: '1.0.0', - private: true, - workspaces: ['packages/*'], - scripts: { - 'build:all': 'vite run build', - 'test:all': 'vite run test', - 'lint:all': 'vite run lint', - }, - devDependencies: { - '@voidzero-dev/vite-plus': '*', - }, - }; - - fs.writeFileSync( - path.join(this.rootDir, 'package.json'), - JSON.stringify(rootPackageJson, null, 2), - ); - - // Write pnpm-workspace.yaml for pnpm support - const pnpmWorkspace = `packages: - - 'packages/*' -`; - fs.writeFileSync( - path.join(this.rootDir, 'pnpm-workspace.yaml'), - pnpmWorkspace, - ); - - // Write root vite-plus.json - const rootVitePlusConfig = { - tasks: { - build: { - cache: true, - parallel: true, - }, - test: { - cache: true, - parallel: true, - }, - lint: { - cache: false, - parallel: true, - }, - }, - }; - - fs.writeFileSync( - path.join(this.rootDir, 'vite-plus.json'), - JSON.stringify(rootVitePlusConfig, null, 2), - ); - - console.log( - `Successfully generated monorepo with ${this.PACKAGE_COUNT} packages!`, - ); - console.log(`Location: ${this.rootDir}`); - - // Print some statistics - this.printStatistics(); - } - - private printStatistics(): void { - let totalDeps = 0; - let maxDeps = 0; - let packagesWithVitePlus = 0; - const scriptCounts = new Map(); - - for (const [_, pkg] of this.packages) { - totalDeps += pkg.dependencies.length; - maxDeps = Math.max(maxDeps, pkg.dependencies.length); - - if (pkg.hasVitePlusConfig) { - packagesWithVitePlus++; - } - - for (const script of Object.keys(pkg.scripts)) { - scriptCounts.set(script, (scriptCounts.get(script) || 0) + 1); - } - } - - console.log('\nStatistics:'); - console.log(`- Total packages: ${this.packages.size}`); - console.log( - `- Average dependencies per package: ${ - ( - totalDeps / this.packages.size - ).toFixed(2) - }`, - ); - console.log(`- Max dependencies in a package: ${maxDeps}`); - console.log(`- Packages with vite-plus.json: ${packagesWithVitePlus}`); - console.log('- Script distribution:'); - for (const [script, count] of scriptCounts) { - console.log(` - ${script}: ${count} packages`); - } - } -} - -// Main execution -const outputDir = path.join(__dirname, 'fixtures', 'monorepo'); -const generator = new MonorepoGenerator(outputDir); -generator.generate(); diff --git a/bench/package.json b/bench/package.json deleted file mode 100644 index 3dbc1ca5..00000000 --- a/bench/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/bench/tsconfig.json b/bench/tsconfig.json deleted file mode 100644 index a5cebd34..00000000 --- a/bench/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "noEmit": true, - "erasableSyntaxOnly": false - }, - "include": ["**/*.ts"] -} diff --git a/crates/vite_error/Cargo.toml b/crates/vite_error/Cargo.toml deleted file mode 100644 index 5e8ab60d..00000000 --- a/crates/vite_error/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "vite_error" -version = "0.0.0" -authors.workspace = true -edition.workspace = true -license.workspace = true -publish = false -rust-version.workspace = true - -[dependencies] -anyhow = { workspace = true } -bincode = { workspace = true } -bstr = { workspace = true } -nix = { workspace = true } -rusqlite = { workspace = true } -semver = { workspace = true } -serde_json = { workspace = true } -serde_yml = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } -vite_path = { workspace = true } -vite_str = { workspace = true } -vite_task = { workspace = true } -vite_workspace = { workspace = true } -wax = { workspace = true } - -[target.'cfg(target_os = "windows")'.dependencies] -reqwest = { workspace = true, features = ["stream", "native-tls-vendored", "json"] } - -[target.'cfg(not(target_os = "windows"))'.dependencies] -reqwest = { workspace = true, features = ["stream", "rustls-tls", "json"] } diff --git a/crates/vite_error/src/lib.rs b/crates/vite_error/src/lib.rs deleted file mode 100644 index 260cf60a..00000000 --- a/crates/vite_error/src/lib.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::{ffi::OsString, path::Path, sync::Arc}; - -use thiserror::Error; -use vite_path::{AbsolutePath, AbsolutePathBuf, relative::FromPathError}; -use vite_str::Str; - -#[derive(Error, Debug)] -pub enum Error { - #[error(transparent)] - Sqlite(#[from] rusqlite::Error), - - #[error(transparent)] - BincodeEncode(#[from] bincode::error::EncodeError), - - #[error(transparent)] - BincodeDecode(#[from] bincode::error::DecodeError), - - #[error("Unrecognized db version: {0}")] - UnrecognizedDbVersion(u32), - - #[error(transparent)] - Io(#[from] std::io::Error), - - #[error("IO error: {err} at {path:?}")] - IoWithPath { err: std::io::Error, path: Arc }, - - #[error(transparent)] - JoinPathsError(#[from] std::env::JoinPathsError), - - #[cfg(unix)] - #[error(transparent)] - Nix(#[from] nix::Error), - - #[error(transparent)] - Serde(#[from] serde_json::Error), - - #[error("Env value is not valid unicode: {key} = {value:?}")] - EnvValueIsNotValidUnicode { key: Str, value: OsString }, - - #[cfg(unix)] - #[error("Unsupported file type: {0:?}")] - UnsupportedFileType(nix::dir::Type), - - #[cfg(windows)] - #[error("Unsupported file type: {0:?}")] - UnsupportedFileType(std::fs::FileType), - - #[error(transparent)] - Utf8Error(#[from] bstr::Utf8Error), - - #[error(transparent)] - WaxBuild(#[from] wax::BuildError), - - #[error(transparent)] - WaxWalk(#[from] wax::WalkError), - - #[error(transparent)] - SerdeYml(#[from] serde_yml::Error), - - #[error(transparent)] - TaskError(#[from] vite_task::Error), - - #[error(transparent)] - WorkspaceError(#[from] vite_workspace::Error), - - #[error("Lint failed, reason: {reason}")] - LintFailed { status: Str, reason: Str }, - - #[error("Fmt failed")] - FmtFailed { status: Str, reason: Str }, - - #[error("Vite failed")] - Vite { status: Str, reason: Str }, - - #[error("Test failed")] - TestFailed { status: Str, reason: Str }, - - #[error("Lib failed")] - LibFailed { status: Str, reason: Str }, - - #[error("Doc failed, reason: {reason}")] - DocFailed { status: Str, reason: Str }, - - #[error("Resolve universal vite config failed")] - ResolveUniversalViteConfigFailed { status: Str, reason: Str }, - - #[error("The path ({path:?}) is not a valid relative path because: {reason}")] - InvalidRelativePath { path: Box, reason: FromPathError }, - - #[error("Unsupported package manager: {0}")] - UnsupportedPackageManager(Str), - - #[error("Unrecognized any package manager, please specify the package manager")] - UnrecognizedPackageManager, - - #[error( - "Package manager {name}@{version} in {package_json_path:?} is invalid, expected format: 'package-manager-name@major.minor.patch'" - )] - PackageManagerVersionInvalid { name: Str, version: Str, package_json_path: AbsolutePathBuf }, - - #[error("Package manager {name}@{version} not found on {url}")] - PackageManagerVersionNotFound { name: Str, version: Str, url: Str }, - - #[error(transparent)] - Semver(#[from] semver::Error), - - #[error(transparent)] - Reqwest(#[from] reqwest::Error), - - #[error(transparent)] - JoinError(#[from] tokio::task::JoinError), - - #[error("User cancelled by Ctrl+C")] - UserCancelled, - - #[error("Hash mismatch: expected {expected}, got {actual}")] - HashMismatch { expected: Str, actual: Str }, - - #[error("Invalid hash format: {0}")] - InvalidHashFormat(Str), - - #[error("Unsupported hash algorithm: {0}")] - UnsupportedHashAlgorithm(Str), - - #[error(transparent)] - Anyhow(#[from] anyhow::Error), -} diff --git a/crates/vite_install/Cargo.toml b/crates/vite_install/Cargo.toml deleted file mode 100644 index 15cfd2ee..00000000 --- a/crates/vite_install/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "vite_install" -version = "0.0.0" -authors.workspace = true -edition.workspace = true -license.workspace = true -publish = false -rust-version.workspace = true - -[dependencies] -backon = { workspace = true } -directories = { workspace = true } -flate2 = { workspace = true } -futures-util = { workspace = true } -hex = { workspace = true } -indoc = { workspace = true } -pathdiff = { workspace = true } -semver = { workspace = true } -serde = { workspace = true, features = ["derive"] } -# use `preserve_order` feature to preserve the order of the fields in `package.json` -serde_json = { workspace = true, features = ["preserve_order"] } -sha1 = { workspace = true } -sha2 = { workspace = true } -tar = { workspace = true } -tempfile = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } -vite_error = { workspace = true } -vite_glob = { workspace = true } -vite_path = { workspace = true } -vite_str = { workspace = true } -vite_workspace = { workspace = true } - -[target.'cfg(target_os = "windows")'.dependencies] -reqwest = { workspace = true, features = ["stream", "native-tls-vendored", "json"] } - -[target.'cfg(not(target_os = "windows"))'.dependencies] -reqwest = { workspace = true, features = ["stream", "rustls-tls", "json"] } -nix = { workspace = true } - -[dev-dependencies] -httpmock = { workspace = true } -tempfile = { workspace = true } -test-log = { workspace = true } - -[lints] -workspace = true diff --git a/crates/vite_install/README.md b/crates/vite_install/README.md deleted file mode 100644 index 14e642d7..00000000 --- a/crates/vite_install/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# vite_install - -- Auto-detects package manager type and version from package.json's `packageManager` field -- Downloads and caches the specified version -- Handles install, add, etc. commands for pnpm/yarn/npm. diff --git a/crates/vite_install/src/commands/add.rs b/crates/vite_install/src/commands/add.rs deleted file mode 100644 index 5cbf0016..00000000 --- a/crates/vite_install/src/commands/add.rs +++ /dev/null @@ -1,559 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// The type of dependency to save. -#[derive(Debug, Default, Clone, Copy)] -pub enum SaveDependencyType { - /// Save as dependencies. - #[default] - Production, - /// Save as devDependencies. - Dev, - /// Save as peerDependencies. - Peer, - /// Save as optionalDependencies. - Optional, -} - -#[derive(Debug, Default)] -pub struct AddCommandOptions<'a> { - pub packages: &'a [String], - pub save_dependency_type: Option, - pub save_exact: bool, - pub save_catalog_name: Option<&'a str>, - pub filters: Option<&'a [String]>, - pub workspace_root: bool, - pub workspace_only: bool, - pub global: bool, - pub allow_build: Option<&'a str>, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the add command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_add_command( - &self, - options: &AddCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_add_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the add command. - #[must_use] - pub fn resolve_add_command(&self, options: &AddCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - // global packages should use npm cli only - if options.global { - bin_name = "npm".into(); - args.push("install".into()); - args.push("--global".into()); - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - args.extend_from_slice(options.packages); - - return ResolveCommandResult { bin_path: bin_name, args, envs }; - } - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - // pnpm: --filter must come before command - if let Some(filters) = options.filters { - for filter in filters { - args.push("--filter".into()); - args.push(filter.clone()); - } - } - args.push("add".into()); - if options.workspace_root { - args.push("--workspace-root".into()); - } - if options.workspace_only { - args.push("--workspace".into()); - } - - // https://pnpm.io/cli/add#options - if let Some(save_dependency_type) = options.save_dependency_type { - match save_dependency_type { - SaveDependencyType::Production => { - args.push("--save-prod".into()); - } - SaveDependencyType::Dev => { - args.push("--save-dev".into()); - } - SaveDependencyType::Peer => { - args.push("--save-peer".into()); - } - SaveDependencyType::Optional => { - args.push("--save-optional".into()); - } - } - } - if options.save_exact { - args.push("--save-exact".into()); - } - - if let Some(save_catalog_name) = options.save_catalog_name { - if save_catalog_name.is_empty() { - args.push("--save-catalog".into()); - } else { - args.push(format!("--save-catalog-name={}", save_catalog_name)); - } - } - - if let Some(allow_build) = options.allow_build { - args.push(format!("--allow-build={}", allow_build)); - } - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - // yarn: workspaces foreach --all --include {filter} add - // https://yarnpkg.com/cli/workspaces/foreach - if let Some(filters) = options.filters { - args.push("workspaces".into()); - args.push("foreach".into()); - args.push("--all".into()); - for filter in filters { - args.push("--include".into()); - args.push(filter.clone()); - } - } - args.push("add".into()); - - // https://yarnpkg.com/cli/add#options - if let Some(save_dependency_type) = options.save_dependency_type { - match save_dependency_type { - SaveDependencyType::Production => { - // default - // no need to add anything - } - SaveDependencyType::Dev => { - args.push("--dev".into()); - } - SaveDependencyType::Peer => { - args.push("--peer".into()); - } - SaveDependencyType::Optional => { - args.push("--optional".into()); - } - } - } - if options.save_exact { - args.push("--exact".into()); - } - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - // npm: install --workspace - args.push("install".into()); - if let Some(filters) = options.filters { - for filter in filters { - args.push("--workspace".into()); - args.push(filter.clone()); - } - } - // https://docs.npmjs.com/cli/v11/commands/npm-install#include-workspace-root - if options.workspace_root { - args.push("--include-workspace-root".into()); - } - - // https://docs.npmjs.com/cli/v11/commands/npm-install#configuration - if let Some(save_dependency_type) = options.save_dependency_type { - match save_dependency_type { - SaveDependencyType::Production => { - args.push("--save".into()); - } - SaveDependencyType::Dev => { - args.push("--save-dev".into()); - } - SaveDependencyType::Peer => { - args.push("--save-peer".into()); - } - SaveDependencyType::Optional => { - args.push("--save-optional".into()); - } - } - } - - if options.save_exact { - args.push("--save-exact".into()); - } - } - } - - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - args.extend_from_slice(options.packages); - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from("1.0.0"), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_basic_add() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: None, - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["add", "react"]); - } - - #[test] - fn test_pnpm_add_with_filter() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["--filter", "app", "add", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_add_with_save_catalog_name() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: Some("react18"), - allow_build: None, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec!["--filter", "app", "add", "--save-catalog-name=react18", "react"] - ); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_add_with_save_catalog_name_and_empty_name() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: Some(""), - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["--filter", "app", "add", "--save-catalog", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_add_with_filter_and_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: true, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["--filter", "app", "add", "--workspace-root", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_add_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["typescript".to_string()], - save_dependency_type: Some(SaveDependencyType::Dev), - save_exact: false, - filters: None, - workspace_root: true, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["add", "--workspace-root", "--save-dev", "typescript"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_add_workspace_only() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["@myorg/utils".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: false, - workspace_only: true, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["--filter", "app", "add", "--workspace", "@myorg/utils"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_yarn_basic_add() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: None, - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["add", "react"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_add_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec!["workspaces", "foreach", "--all", "--include", "app", "add", "react"] - ); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_add_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["typescript".to_string()], - save_dependency_type: Some(SaveDependencyType::Dev), - save_exact: false, - filters: None, - workspace_root: true, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["add", "--dev", "typescript"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_npm_basic_add() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: None, - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["install", "react"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_add_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string()]), - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["install", "--workspace", "app", "react"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_add_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["typescript".to_string()], - save_dependency_type: None, - save_exact: false, - filters: None, - workspace_root: true, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["install", "--include-workspace-root", "typescript"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_add_multiple_workspaces() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["lodash".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string(), "web".to_string()]), - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec!["install", "--workspace", "app", "--workspace", "web", "lodash"] - ); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_add_multiple_workspaces_and_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["lodash".to_string()], - save_dependency_type: None, - save_exact: false, - filters: Some(&["app".to_string(), "web".to_string()]), - workspace_root: true, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: None, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec![ - "install", - "--workspace", - "app", - "--workspace", - "web", - "--include-workspace-root", - "lodash" - ] - ); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_pnpm_add_with_allow_build() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_add_command(&AddCommandOptions { - packages: &["react".to_string()], - save_dependency_type: None, - save_exact: false, - filters: None, - workspace_root: false, - workspace_only: false, - global: false, - save_catalog_name: None, - allow_build: Some("react,napi"), - pass_through_args: None, - }); - assert_eq!(result.args, vec!["add", "--allow-build=react,napi", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } -} diff --git a/crates/vite_install/src/commands/dedupe.rs b/crates/vite_install/src/commands/dedupe.rs deleted file mode 100644 index 2a4c6fa8..00000000 --- a/crates/vite_install/src/commands/dedupe.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Options for the dedupe command. -#[derive(Debug, Default)] -pub struct DedupeCommandOptions<'a> { - pub check: bool, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the dedupe command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_dedupe_command( - &self, - options: &DedupeCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_dedupe_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the dedupe command. - #[must_use] - pub fn resolve_dedupe_command(&self, options: &DedupeCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - args.push("dedupe".into()); - - // pnpm uses --check for dry-run - if options.check { - args.push("--check".into()); - } - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - args.push("dedupe".into()); - - // yarn@2+ supports --check - if options.check { - args.push("--check".into()); - } - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - args.push("dedupe".into()); - - if options.check { - args.push("--dry-run".into()); - } - } - } - - // Add pass-through args - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from(version), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_dedupe_basic() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_dedupe_command(&DedupeCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["dedupe"]); - } - - #[test] - fn test_pnpm_dedupe_check() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = - pm.resolve_dedupe_command(&DedupeCommandOptions { check: true, ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["dedupe", "--check"]); - } - - #[test] - fn test_npm_dedupe_basic() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_dedupe_command(&DedupeCommandOptions { ..Default::default() }); - assert_eq!(result.args, vec!["dedupe"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_dedupe_check() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = - pm.resolve_dedupe_command(&DedupeCommandOptions { check: true, ..Default::default() }); - assert_eq!(result.args, vec!["dedupe", "--dry-run"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_yarn_dedupe_basic() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_dedupe_command(&DedupeCommandOptions { ..Default::default() }); - assert_eq!(result.args, vec!["dedupe"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_dedupe_check() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = - pm.resolve_dedupe_command(&DedupeCommandOptions { check: true, ..Default::default() }); - assert_eq!(result.args, vec!["dedupe", "--check"]); - assert_eq!(result.bin_path, "yarn"); - } -} diff --git a/crates/vite_install/src/commands/install.rs b/crates/vite_install/src/commands/install.rs deleted file mode 100644 index bd00ec84..00000000 --- a/crates/vite_install/src/commands/install.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::{collections::HashMap, iter}; - -use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; - -impl PackageManager { - /// Resolve the install command. - pub fn resolve_install_command(&self, args: &Vec) -> ResolveCommandResult { - ResolveCommandResult { - bin_path: self.bin_name.to_string(), - args: iter::once("install") - .chain(args.iter().map(String::as_str)) - .map(String::from) - .collect(), - envs: HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]), - } - } -} diff --git a/crates/vite_install/src/commands/link.rs b/crates/vite_install/src/commands/link.rs deleted file mode 100644 index eebc0725..00000000 --- a/crates/vite_install/src/commands/link.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Options for the link command. -#[derive(Debug, Default)] -pub struct LinkCommandOptions<'a> { - pub package: Option<&'a str>, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the link command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_link_command( - &self, - options: &LinkCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_link_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the link command. - #[must_use] - pub fn resolve_link_command(&self, options: &LinkCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - args.push("link".into()); - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - args.push("link".into()); - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - args.push("link".into()); - } - } - - // Add package/directory if specified - if let Some(package) = options.package { - args.push(package.to_string()); - } - - // Add pass-through args - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from(version), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_link_no_package() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["link"]); - } - - #[test] - fn test_pnpm_link_package() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { - package: Some("react"), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["link", "react"]); - } - - #[test] - fn test_pnpm_link_directory() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { - package: Some("./packages/utils"), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["link", "./packages/utils"]); - } - - #[test] - fn test_pnpm_link_absolute_directory() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { - package: Some("/absolute/path/to/package"), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["link", "/absolute/path/to/package"]); - } - - #[test] - fn test_yarn_link_basic() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["link"]); - } - - #[test] - fn test_yarn_link_package() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { - package: Some("react"), - ..Default::default() - }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["link", "react"]); - } - - #[test] - fn test_npm_link_basic() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["link"]); - } - - #[test] - fn test_npm_link_package() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_link_command(&LinkCommandOptions { - package: Some("react"), - ..Default::default() - }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["link", "react"]); - } -} diff --git a/crates/vite_install/src/commands/mod.rs b/crates/vite_install/src/commands/mod.rs deleted file mode 100644 index ca9b7e12..00000000 --- a/crates/vite_install/src/commands/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod add; -pub mod dedupe; -mod install; -pub mod link; -pub mod outdated; -pub mod remove; -pub mod unlink; -pub mod update; -pub mod why; diff --git a/crates/vite_install/src/commands/outdated.rs b/crates/vite_install/src/commands/outdated.rs deleted file mode 100644 index f748be52..00000000 --- a/crates/vite_install/src/commands/outdated.rs +++ /dev/null @@ -1,521 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus, str::FromStr}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Output format for the outdated command. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Format { - /// Table format (default) - Table, - /// List format (parseable) - List, - /// JSON format - Json, -} - -impl Format { - /// Convert format to string representation - pub const fn as_str(self) -> &'static str { - match self { - Format::Table => "table", - Format::List => "list", - Format::Json => "json", - } - } -} - -impl FromStr for Format { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "table" => Ok(Format::Table), - "list" => Ok(Format::List), - "json" => Ok(Format::Json), - _ => Err(format!("Invalid format '{}'. Valid formats: table, list, json", s)), - } - } -} - -impl std::fmt::Display for Format { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// Options for the outdated command. -#[derive(Debug, Default)] -pub struct OutdatedCommandOptions<'a> { - pub packages: &'a [String], - pub long: bool, - pub format: Option, - pub recursive: bool, - pub filters: Option<&'a [String]>, - pub workspace_root: bool, - pub prod: bool, - pub dev: bool, - pub no_optional: bool, - pub compatible: bool, - pub sort_by: Option<&'a str>, - pub global: bool, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the outdated command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_outdated_command( - &self, - options: &OutdatedCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_outdated_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the outdated command. - #[must_use] - pub fn resolve_outdated_command( - &self, - options: &OutdatedCommandOptions, - ) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - // Global packages should use npm cli only - if options.global { - bin_name = "npm".into(); - Self::format_npm_outdated_args(&mut args, options); - args.push("-g".into()); - } else { - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - - // pnpm: --filter must come before command - if let Some(filters) = options.filters { - for filter in filters { - args.push("--filter".into()); - args.push(filter.clone()); - } - } - - args.push("outdated".into()); - - // Handle format option - if let Some(format) = options.format { - args.push("--format".into()); - args.push(format.as_str().into()); - } - - if options.long { - args.push("--long".into()); - } - - if options.workspace_root { - args.push("--workspace-root".into()); - } - - if options.recursive { - args.push("--recursive".into()); - } - - if options.prod { - args.push("--prod".into()); - } - - if options.dev { - args.push("--dev".into()); - } - - if options.no_optional { - args.push("--no-optional".into()); - } - - if options.compatible { - args.push("--compatible".into()); - } - - if let Some(sort_by) = options.sort_by { - args.push("--sort-by".into()); - args.push(sort_by.into()); - } - - // Add packages (pnpm supports glob patterns) - args.extend_from_slice(options.packages); - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - - // Check if yarn@2+ (uses upgrade-interactive) - if !self.version.starts_with("1.") { - println!( - "Note: yarn@2+ uses 'yarn upgrade-interactive' for checking outdated packages" - ); - args.push("upgrade-interactive".into()); - - // Warn about unsupported flags - if options.format.is_some() { - println!("Warning: --format not supported by yarn@2+"); - } - } else { - // yarn@1 - args.push("outdated".into()); - - // Add packages (yarn@1 supports package names) - args.extend_from_slice(options.packages); - - // yarn@1 supports --json format - if let Some(format) = options.format { - match format { - Format::Json => args.push("--json".into()), - Format::List => { - println!("Warning: yarn@1 not support list format"); - } - Format::Table => {} // Default, no flag needed - } - } - } - - // Common warnings - if options.long { - println!("Warning: --long not supported by yarn"); - } - if options.workspace_root { - println!("Warning: --workspace-root not supported by yarn"); - } - if options.recursive { - println!("Warning: --recursive not supported by yarn"); - } - if let Some(filters) = options.filters { - if !filters.is_empty() { - println!("Warning: --filter not supported by yarn"); - } - } - if options.prod || options.dev { - println!("Warning: --prod/--dev not supported by yarn"); - } - if options.no_optional { - println!("Warning: --no-optional not supported by yarn"); - } - if options.compatible { - println!("Warning: --compatible not supported by yarn"); - } - if options.sort_by.is_some() { - println!("Warning: --sort-by not supported by yarn"); - } - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - Self::format_npm_outdated_args(&mut args, options); - } - } - } - - // Add pass-through args - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - - ResolveCommandResult { bin_path: bin_name, args, envs } - } - - fn format_npm_outdated_args(args: &mut Vec, options: &OutdatedCommandOptions) { - args.push("outdated".into()); - - // npm format flags - translate from --format - if let Some(format) = options.format { - match format { - Format::Json => args.push("--json".into()), - Format::List => args.push("--parseable".into()), - Format::Table => {} // Default, no flag needed - } - } - - if options.long { - args.push("--long".into()); - } - - // npm workspace flags - translate from --filter - if let Some(filters) = options.filters { - for filter in filters { - args.push("--workspace".into()); - args.push(filter.clone()); - } - } - - // npm uses --include-workspace-root when workspace_root is set - if options.workspace_root { - args.push("--include-workspace-root".into()); - } - - // npm --all translates from -r/--recursive - if options.recursive { - args.push("--all".into()); - } - - // Add packages (npm supports package names) - args.extend_from_slice(options.packages); - - // Warn about pnpm-specific flags - if options.prod || options.dev { - println!("Warning: --prod/--dev not supported by npm"); - } - if options.no_optional { - println!("Warning: --no-optional not supported by npm"); - } - if options.compatible { - println!("Warning: --compatible not supported by npm"); - } - if options.sort_by.is_some() { - println!("Warning: --sort-by not supported by npm"); - } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from(version), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_outdated_basic() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["outdated"]); - } - - #[test] - fn test_pnpm_outdated_with_packages() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["*babel*".to_string(), "eslint-*".to_string()]; - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - packages: &packages, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["outdated", "*babel*", "eslint-*"]); - } - - #[test] - fn test_pnpm_outdated_json() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - format: Some(Format::Json), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["outdated", "--format", "json"]); - } - - #[test] - fn test_npm_outdated_basic() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { ..Default::default() }); - assert_eq!(result.args, vec!["outdated"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_outdated_json() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - format: Some(Format::Json), - ..Default::default() - }); - assert_eq!(result.args, vec!["outdated", "--json"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_yarn_outdated_basic() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.19"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { ..Default::default() }); - assert_eq!(result.args, vec!["outdated"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_pnpm_outdated_with_filter() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let filters = vec!["app".to_string()]; - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - filters: Some(&filters), - recursive: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["--filter", "app", "outdated", "--recursive"]); - } - - #[test] - fn test_pnpm_outdated_prod_only() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm - .resolve_outdated_command(&OutdatedCommandOptions { prod: true, ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["outdated", "--prod"]); - } - - #[test] - fn test_npm_outdated_list_format() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - format: Some(Format::List), - ..Default::default() - }); - assert_eq!(result.args, vec!["outdated", "--parseable"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_outdated_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - recursive: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["outdated", "--all"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_outdated_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let filters = vec!["app".to_string()]; - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - filters: Some(&filters), - ..Default::default() - }); - assert_eq!(result.args, vec!["outdated", "--workspace", "app"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_global_outdated() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - global: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["outdated", "-g"]); - } - - #[test] - fn test_pnpm_outdated_with_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - workspace_root: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["outdated", "--workspace-root"]); - } - - #[test] - fn test_pnpm_outdated_with_workspace_root_and_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - workspace_root: true, - recursive: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["outdated", "--workspace-root", "--recursive"]); - } - - #[test] - fn test_pnpm_outdated_with_all_flags() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let filters = vec!["app".to_string()]; - let packages = vec!["react".to_string()]; - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - packages: &packages, - long: true, - format: Some(Format::Json), - recursive: true, - filters: Some(&filters), - workspace_root: true, - prod: true, - compatible: true, - sort_by: Some("name"), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!( - result.args, - vec![ - "--filter", - "app", - "outdated", - "--format", - "json", - "--long", - "--workspace-root", - "--recursive", - "--prod", - "--compatible", - "--sort-by", - "name", - "react" - ] - ); - } - - #[test] - fn test_npm_outdated_with_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - workspace_root: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["outdated", "--include-workspace-root"]); - } - - #[test] - fn test_npm_outdated_with_workspace_root_and_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let filters = vec!["app".to_string()]; - let result = pm.resolve_outdated_command(&OutdatedCommandOptions { - filters: Some(&filters), - workspace_root: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["outdated", "--workspace", "app", "--include-workspace-root"]); - } -} diff --git a/crates/vite_install/src/commands/remove.rs b/crates/vite_install/src/commands/remove.rs deleted file mode 100644 index 4cf8e9b1..00000000 --- a/crates/vite_install/src/commands/remove.rs +++ /dev/null @@ -1,660 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Options for the remove command. -#[derive(Debug, Default)] -pub struct RemoveCommandOptions<'a> { - pub packages: &'a [String], - pub filters: Option<&'a [String]>, - pub workspace_root: bool, - pub recursive: bool, - pub global: bool, - pub save_dev: bool, - pub save_optional: bool, - pub save_prod: bool, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the remove command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_remove_command( - &self, - options: &RemoveCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_remove_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the remove command. - #[must_use] - pub fn resolve_remove_command(&self, options: &RemoveCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - // global packages should use npm cli only - if options.global { - // TODO(@fengmk2): Need to handle the case where the npm CLI does not exist in the PATH - bin_name = "npm".into(); - args.push("uninstall".into()); - args.push("--global".into()); - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - args.extend_from_slice(options.packages); - - return ResolveCommandResult { bin_path: bin_name, args, envs }; - } - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - // pnpm: --filter must come before command - if let Some(filters) = options.filters { - for filter in filters { - args.push("--filter".into()); - args.push(filter.clone()); - } - } - args.push("remove".into()); - if options.workspace_root { - args.push("--workspace-root".into()); - } - if options.recursive { - args.push("--recursive".into()); - } - // https://pnpm.io/cli/remove#options - if options.save_dev { - args.push("--save-dev".into()); - } - if options.save_optional { - args.push("--save-optional".into()); - } - if options.save_prod { - args.push("--save-prod".into()); - } - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - // NOTE: filters are not supported in recursive mode - // yarn: workspaces foreach --all --include {filter} remove - // https://yarnpkg.com/cli/workspace - if let Some(filters) = options.filters - && !options.recursive - { - args.push("workspaces".into()); - args.push("foreach".into()); - args.push("--all".into()); - for filter in filters { - args.push("--include".into()); - args.push(filter.clone()); - } - } - args.push("remove".into()); - if options.recursive { - args.push("--all".into()); - } - // NOTE: yarn doesn't support -w flag for workspace root in remove command - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - // npm: uninstall --workspace - args.push("uninstall".into()); - if let Some(filters) = options.filters { - for filter in filters { - args.push("--workspace".into()); - args.push(filter.clone()); - } - } - // https://docs.npmjs.com/cli/v11/commands/npm-uninstall#configuration - if options.workspace_root || options.recursive { - // recursive mode will remove from workspace root - args.push("--include-workspace-root".into()); - } - if options.recursive { - args.push("--workspaces".into()); - } - // not support: save_dev, save_optional, save_prod, just ignore them - } - } - - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - args.extend_from_slice(options.packages); - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from("1.0.0"), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_basic_remove() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["remove", "lodash"]); - } - - #[test] - fn test_pnpm_remove_with_filter() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string()]), - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["--filter", "app", "remove", "lodash"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_remove_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["typescript".to_string()], - filters: None, - workspace_root: true, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--workspace-root", "typescript"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_remove_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: true, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--recursive", "lodash"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_remove_multiple_filters() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["axios".to_string()], - filters: Some(&["app".to_string(), "web".to_string()]), - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["--filter", "app", "--filter", "web", "remove", "axios"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_yarn_basic_remove() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "lodash"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_remove_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string()]), - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec!["workspaces", "foreach", "--all", "--include", "app", "remove", "lodash"] - ); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_remove_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: true, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--all", "lodash"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_npm_basic_remove() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "lodash"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_remove_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string()]), - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "--workspace", "app", "lodash"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_remove_workspace_root() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["typescript".to_string()], - filters: None, - workspace_root: true, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "--include-workspace-root", "typescript"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_remove_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: true, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec!["uninstall", "--include-workspace-root", "--workspaces", "lodash"] - ); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_remove_multiple_workspaces() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string(), "web".to_string()]), - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec!["uninstall", "--workspace", "app", "--workspace", "web", "lodash"] - ); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_global_remove() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["typescript".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: true, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "--global", "typescript"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_remove_multiple_packages() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string(), "axios".to_string(), "underscore".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "lodash", "axios", "underscore"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_remove_with_pass_through_args() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: Some(&["--use-stderr".to_string()]), - }); - assert_eq!(result.args, vec!["remove", "--use-stderr", "lodash"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_remove_save_dev() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["typescript".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: true, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--save-dev", "typescript"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_remove_save_optional() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["sharp".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: true, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--save-optional", "sharp"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_remove_save_prod() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["react".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: true, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--save-prod", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_npm_remove_save_dev() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["typescript".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: true, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "typescript"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_remove_save_optional() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["sharp".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: true, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "sharp"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_remove_save_prod() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["react".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: true, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["uninstall", "react"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_yarn_remove_save_flags_ignored() { - // Yarn doesn't support save flags, so they should be ignored - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: false, - global: false, - save_dev: true, - save_optional: true, - save_prod: true, - pass_through_args: None, - }); - // Should not include any save flags for yarn - assert_eq!(result.args, vec!["remove", "lodash"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_remove_with_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: None, - workspace_root: false, - recursive: true, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!(result.args, vec!["remove", "--all", "lodash"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_remove_with_multiple_filters() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string(), "web".to_string()]), - workspace_root: false, - recursive: false, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - assert_eq!( - result.args, - vec![ - "workspaces", - "foreach", - "--all", - "--include", - "app", - "--include", - "web", - "remove", - "lodash" - ] - ); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_remove_with_recursive_and_multiple_filters() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let result = pm.resolve_remove_command(&RemoveCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string(), "web".to_string()]), - workspace_root: false, - recursive: true, - global: false, - save_dev: false, - save_optional: false, - save_prod: false, - pass_through_args: None, - }); - // ignore filters in recursive mode - assert_eq!(result.args, vec!["remove", "--all", "lodash"]); - assert_eq!(result.bin_path, "yarn"); - } -} diff --git a/crates/vite_install/src/commands/unlink.rs b/crates/vite_install/src/commands/unlink.rs deleted file mode 100644 index 8dc55c5e..00000000 --- a/crates/vite_install/src/commands/unlink.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Options for the unlink command. -#[derive(Debug, Default)] -pub struct UnlinkCommandOptions<'a> { - pub package: Option<&'a str>, - pub recursive: bool, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the unlink command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_unlink_command( - &self, - options: &UnlinkCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_unlink_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the unlink command. - #[must_use] - pub fn resolve_unlink_command(&self, options: &UnlinkCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - args.push("unlink".into()); - - if options.recursive { - args.push("--recursive".into()); - } - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - args.push("unlink".into()); - - if options.recursive { - args.push("--all".into()); - } - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - args.push("unlink".into()); - - if options.recursive { - println!("Warning: npm doesn't support --recursive for unlink command"); - } - } - } - - // Add package if specified - if let Some(package) = options.package { - args.push(package.to_string()); - } - - // Add pass-through args - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from(version), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_unlink_no_package() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["unlink"]); - } - - #[test] - fn test_pnpm_unlink_package() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { - package: Some("react"), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["unlink", "react"]); - } - - #[test] - fn test_pnpm_unlink_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { - recursive: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["unlink", "--recursive"]); - } - - #[test] - fn test_pnpm_unlink_package_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { - package: Some("react"), - recursive: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["unlink", "--recursive", "react"]); - } - - #[test] - fn test_yarn_unlink_basic() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["unlink"]); - } - - #[test] - fn test_yarn_unlink_package() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { - package: Some("react"), - ..Default::default() - }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["unlink", "react"]); - } - - #[test] - fn test_yarn_unlink_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { - recursive: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["unlink", "--all"]); - } - - #[test] - fn test_npm_unlink_basic() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["unlink"]); - } - - #[test] - fn test_npm_unlink_package() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_unlink_command(&UnlinkCommandOptions { - package: Some("react"), - ..Default::default() - }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["unlink", "react"]); - } -} diff --git a/crates/vite_install/src/commands/update.rs b/crates/vite_install/src/commands/update.rs deleted file mode 100644 index 7c211c1e..00000000 --- a/crates/vite_install/src/commands/update.rs +++ /dev/null @@ -1,595 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Options for the update command. -#[derive(Debug, Default)] -pub struct UpdateCommandOptions<'a> { - pub packages: &'a [String], - pub latest: bool, - pub global: bool, - pub recursive: bool, - pub filters: Option<&'a [String]>, - pub workspace_root: bool, - pub dev: bool, - pub prod: bool, - pub interactive: bool, - pub no_optional: bool, - pub no_save: bool, - pub workspace_only: bool, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the update command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_update_command( - &self, - options: &UpdateCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_update_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the update command. - #[must_use] - pub fn resolve_update_command(&self, options: &UpdateCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - // global packages should use npm cli only - if options.global { - bin_name = "npm".into(); - args.push("update".into()); - args.push("--global".into()); - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - args.extend_from_slice(options.packages); - - return ResolveCommandResult { bin_path: bin_name, args, envs }; - } - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - // pnpm: --filter must come before command - if let Some(filters) = options.filters { - for filter in filters { - args.push("--filter".into()); - args.push(filter.clone()); - } - } - args.push("update".into()); - - if options.latest { - args.push("--latest".into()); - } - if options.workspace_root { - args.push("--workspace-root".into()); - } - if options.recursive { - args.push("--recursive".into()); - } - if options.dev { - args.push("--dev".into()); - } - if options.prod { - args.push("--prod".into()); - } - if options.interactive { - args.push("--interactive".into()); - } - if options.no_optional { - args.push("--no-optional".into()); - } - if options.no_save { - args.push("--no-save".into()); - } - if options.workspace_only { - args.push("--workspace".into()); - } - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - - // Determine yarn version - let is_yarn_v1 = self.version.starts_with("1."); - - if is_yarn_v1 { - // yarn@1: yarn upgrade [--latest] - if let Some(filters) = options.filters { - args.push("workspace".into()); - args.push(filters[0].clone()); - } - args.push("upgrade".into()); - if options.latest { - args.push("--latest".into()); - } - } else { - // yarn@2+: yarn up (already updates to latest by default) - if let Some(filters) = options.filters { - args.push("workspaces".into()); - args.push("foreach".into()); - args.push("--all".into()); - for filter in filters { - args.push("--include".into()); - args.push(filter.clone()); - } - } - args.push("up".into()); - if options.recursive { - args.push("--recursive".into()); - } - if options.interactive { - args.push("--interactive".into()); - } - } - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - args.push("update".into()); - - if let Some(filters) = options.filters { - for filter in filters { - args.push("--workspace".into()); - args.push(filter.clone()); - } - } - if options.workspace_root || options.recursive { - args.push("--include-workspace-root".into()); - } - if options.recursive { - args.push("--workspaces".into()); - } - if options.dev { - args.push("--include=dev".into()); - } - if options.prod { - args.push("--include=prod".into()); - } - if options.no_optional { - args.push("--no-optional".into()); - } - if options.no_save { - args.push("--no-save".into()); - } - - // npm doesn't have --latest flag - // Warn user or handle differently - if options.latest { - println!( - "Warning: npm doesn't support --latest flag. Updating within semver range only." - ); - } - - // npm doesn't support interactive mode - if options.interactive { - println!( - "Warning: npm doesn't support interactive mode. Running standard update." - ); - } - } - } - - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - args.extend_from_slice(options.packages); - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from(version), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_basic_update() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - latest: false, - global: false, - recursive: false, - filters: None, - workspace_root: false, - dev: false, - prod: false, - interactive: false, - no_optional: false, - no_save: false, - workspace_only: false, - pass_through_args: None, - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["update", "react"]); - } - - #[test] - fn test_pnpm_update_latest() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - latest: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["update", "--latest", "react"]); - } - - #[test] - fn test_pnpm_update_all() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - latest: false, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["update"]); - } - - #[test] - fn test_pnpm_update_with_filter() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - filters: Some(&["app".to_string()]), - ..Default::default() - }); - assert_eq!(result.args, vec!["--filter", "app", "update", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - recursive: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--recursive"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_interactive() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - interactive: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--interactive"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_dev_only() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - dev: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--dev"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_no_optional() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - no_optional: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--no-optional"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_no_save() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - no_save: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--no-save", "react"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_workspace_only() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["@myorg/utils".to_string()], - workspace_only: true, - filters: Some(&["app".to_string()]), - ..Default::default() - }); - assert_eq!(result.args, vec!["--filter", "app", "update", "--workspace", "@myorg/utils"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_yarn_v1_basic_update() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - ..Default::default() - }); - assert_eq!(result.args, vec!["upgrade", "react"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_v1_update_latest() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - latest: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["upgrade", "--latest", "react"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_v1_update_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - filters: Some(&["app".to_string()]), - ..Default::default() - }); - assert_eq!(result.args, vec!["workspace", "app", "upgrade", "react"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_v4_basic_update() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - ..Default::default() - }); - assert_eq!(result.args, vec!["up", "react"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_v4_update_interactive() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - interactive: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["up", "--interactive"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_v4_update_with_filter() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - filters: Some(&["app".to_string()]), - ..Default::default() - }); - assert_eq!( - result.args, - vec!["workspaces", "foreach", "--all", "--include", "app", "up", "react"] - ); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_yarn_v4_update_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - recursive: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["up", "--recursive"]); - assert_eq!(result.bin_path, "yarn"); - } - - #[test] - fn test_npm_basic_update() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "react"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_update_all() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm - .resolve_update_command(&UpdateCommandOptions { packages: &[], ..Default::default() }); - assert_eq!(result.args, vec!["update"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_update_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - filters: Some(&["app".to_string()]), - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--workspace", "app", "react"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_update_recursive() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - recursive: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--include-workspace-root", "--workspaces"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_update_dev_only() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - dev: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--include=dev"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_update_no_optional() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &[], - no_optional: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--no-optional"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_npm_update_no_save() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - no_save: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--no-save", "react"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_global_update() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["typescript".to_string()], - global: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--global", "typescript"]); - assert_eq!(result.bin_path, "npm"); - } - - #[test] - fn test_pnpm_update_multiple_packages() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string(), "react-dom".to_string(), "vite".to_string()], - latest: true, - ..Default::default() - }); - assert_eq!(result.args, vec!["update", "--latest", "react", "react-dom", "vite"]); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_pnpm_update_complex() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["react".to_string()], - latest: true, - recursive: true, - filters: Some(&["app".to_string(), "web".to_string()]), - dev: true, - interactive: true, - ..Default::default() - }); - assert_eq!( - result.args, - vec![ - "--filter", - "app", - "--filter", - "web", - "update", - "--latest", - "--recursive", - "--dev", - "--interactive", - "react" - ] - ); - assert_eq!(result.bin_path, "pnpm"); - } - - #[test] - fn test_yarn_v4_update_multiple_filters() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let result = pm.resolve_update_command(&UpdateCommandOptions { - packages: &["lodash".to_string()], - filters: Some(&["app".to_string(), "web".to_string()]), - ..Default::default() - }); - assert_eq!( - result.args, - vec![ - "workspaces", - "foreach", - "--all", - "--include", - "app", - "--include", - "web", - "up", - "lodash" - ] - ); - assert_eq!(result.bin_path, "yarn"); - } -} diff --git a/crates/vite_install/src/commands/why.rs b/crates/vite_install/src/commands/why.rs deleted file mode 100644 index 6cc8e630..00000000 --- a/crates/vite_install/src/commands/why.rs +++ /dev/null @@ -1,380 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_error::Error; -use vite_path::AbsolutePath; - -use crate::package_manager::{ - PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, -}; - -/// Options for the why command. -#[derive(Debug, Default)] -pub struct WhyCommandOptions<'a> { - pub packages: &'a [String], - pub json: bool, - pub long: bool, - pub parseable: bool, - pub recursive: bool, - pub filters: Option<&'a [String]>, - pub workspace_root: bool, - pub prod: bool, - pub dev: bool, - pub depth: Option, - pub no_optional: bool, - pub global: bool, - pub exclude_peers: bool, - pub find_by: Option<&'a str>, - pub pass_through_args: Option<&'a [String]>, -} - -impl PackageManager { - /// Run the why command with the package manager. - /// Return the exit status of the command. - #[must_use] - pub async fn run_why_command( - &self, - options: &WhyCommandOptions<'_>, - cwd: impl AsRef, - ) -> Result { - let resolve_command = self.resolve_why_command(options); - run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) - .await - } - - /// Resolve the why command. - #[must_use] - pub fn resolve_why_command(&self, options: &WhyCommandOptions) -> ResolveCommandResult { - let bin_name: String; - let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); - let mut args: Vec = Vec::new(); - - match self.client { - PackageManagerType::Pnpm => { - bin_name = "pnpm".into(); - - // pnpm: --filter must come before command - if let Some(filters) = options.filters { - for filter in filters { - args.push("--filter".into()); - args.push(filter.clone()); - } - } - - args.push("why".into()); - - if options.json { - args.push("--json".into()); - } - - if options.long { - args.push("--long".into()); - } - - if options.parseable { - args.push("--parseable".into()); - } - - if options.recursive { - args.push("--recursive".into()); - } - - if options.workspace_root { - args.push("--workspace-root".into()); - } - - if options.prod { - args.push("--prod".into()); - } - - if options.dev { - args.push("--dev".into()); - } - - if let Some(depth) = options.depth { - args.push("--depth".into()); - args.push(depth.to_string()); - } - - if options.no_optional { - args.push("--no-optional".into()); - } - - if options.global { - args.push("--global".into()); - } - - if options.exclude_peers { - args.push("--exclude-peers".into()); - } - - if let Some(find_by) = options.find_by { - args.push("--find-by".into()); - args.push(find_by.to_string()); - } - - // Add packages (pnpm supports multiple packages) - args.extend_from_slice(options.packages); - } - PackageManagerType::Yarn => { - bin_name = "yarn".into(); - - args.push("why".into()); - - // yarn only supports single package - if options.packages.len() > 1 { - println!( - "Warning: yarn only supports checking one package at a time, using first package" - ); - } - args.push(options.packages[0].clone()); - - // yarn@2+ supports --recursive - if options.recursive && !self.version.starts_with("1.") { - args.push("--recursive".into()); - } - - // yarn@2+: Add --peers by default unless --exclude-peers is set - if !self.version.starts_with("1.") && !options.exclude_peers { - args.push("--peers".into()); - } - - // Warn about unsupported flags - if options.json { - println!("Warning: --json not supported by yarn"); - } - if options.long { - println!("Warning: --long not supported by yarn"); - } - if options.parseable { - println!("Warning: --parseable not supported by yarn"); - } - if let Some(filters) = options.filters { - if !filters.is_empty() { - println!("Warning: --filter not supported by yarn"); - } - } - if options.prod || options.dev { - println!("Warning: --prod/--dev not supported by yarn"); - } - if options.find_by.is_some() { - println!("Warning: --find-by not supported by yarn"); - } - } - PackageManagerType::Npm => { - bin_name = "npm".into(); - - // npm uses 'explain' as primary command - args.push("explain".into()); - - // npm: --workspace comes after command - if let Some(filters) = options.filters { - for filter in filters { - args.push("--workspace".into()); - args.push(filter.clone()); - } - } - - if options.json { - args.push("--json".into()); - } - - // Add packages (npm supports multiple packages) - args.extend_from_slice(options.packages); - - // Warn about pnpm-specific flags - if options.long { - println!("Warning: --long not supported by npm"); - } - if options.parseable { - println!("Warning: --parseable not supported by npm"); - } - if options.prod || options.dev { - println!("Warning: --prod/--dev not supported by npm"); - } - if options.depth.is_some() { - println!("Warning: --depth not supported by npm"); - } - if options.find_by.is_some() { - println!("Warning: --find-by not supported by npm"); - } - } - } - - // Add pass-through args - if let Some(pass_through_args) = options.pass_through_args { - args.extend_from_slice(pass_through_args); - } - - ResolveCommandResult { bin_path: bin_name, args, envs } - } -} - -#[cfg(test)] -mod tests { - use tempfile::{TempDir, tempdir}; - use vite_path::AbsolutePathBuf; - use vite_str::Str; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: Str::from(version), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path.clone(), - install_dir, - } - } - - #[test] - fn test_pnpm_why_basic() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["react".to_string()]; - let result = pm - .resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["why", "react"]); - } - - #[test] - fn test_pnpm_why_multiple_packages() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["react".to_string(), "lodash".to_string()]; - let result = pm - .resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["why", "react", "lodash"]); - } - - #[test] - fn test_pnpm_why_json() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["react".to_string()]; - let result = pm.resolve_why_command(&WhyCommandOptions { - packages: &packages, - json: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["why", "--json", "react"]); - } - - #[test] - fn test_npm_explain_basic() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let packages = vec!["react".to_string()]; - let result = pm - .resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["explain", "react"]); - } - - #[test] - fn test_npm_explain_multiple_packages() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let packages = vec!["react".to_string(), "lodash".to_string()]; - let result = pm - .resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["explain", "react", "lodash"]); - } - - #[test] - fn test_npm_explain_with_workspace() { - let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); - let packages = vec!["react".to_string()]; - let filters = vec!["app".to_string()]; - let result = pm.resolve_why_command(&WhyCommandOptions { - packages: &packages, - filters: Some(&filters), - ..Default::default() - }); - assert_eq!(result.bin_path, "npm"); - assert_eq!(result.args, vec!["explain", "--workspace", "app", "react"]); - } - - #[test] - fn test_yarn_why_basic() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let packages = vec!["react".to_string()]; - let result = pm - .resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["why", "react", "--peers"]); - } - - #[test] - fn test_yarn_why_with_exclude_peers() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); - let packages = vec!["react".to_string()]; - let result = pm.resolve_why_command(&WhyCommandOptions { - packages: &packages, - exclude_peers: true, - ..Default::default() - }); - assert_eq!(result.bin_path, "yarn"); - assert_eq!(result.args, vec!["why", "react"]); - } - - #[test] - fn test_yarn1_why_no_peers() { - let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); - let packages = vec!["react".to_string()]; - let result = pm - .resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() }); - assert_eq!(result.bin_path, "yarn"); - // yarn@1 doesn't support --peers - assert_eq!(result.args, vec!["why", "react"]); - } - - #[test] - fn test_pnpm_why_with_filter() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["react".to_string()]; - let filters = vec!["app".to_string()]; - let result = pm.resolve_why_command(&WhyCommandOptions { - packages: &packages, - filters: Some(&filters), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["--filter", "app", "why", "react"]); - } - - #[test] - fn test_pnpm_why_with_depth() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["react".to_string()]; - let result = pm.resolve_why_command(&WhyCommandOptions { - packages: &packages, - depth: Some(3), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["why", "--depth", "3", "react"]); - } - - #[test] - fn test_pnpm_why_with_find_by() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); - let packages = vec!["react".to_string()]; - let result = pm.resolve_why_command(&WhyCommandOptions { - packages: &packages, - find_by: Some("customFinder"), - ..Default::default() - }); - assert_eq!(result.bin_path, "pnpm"); - assert_eq!(result.args, vec!["why", "--find-by", "customFinder", "react"]); - } -} diff --git a/crates/vite_install/src/config.rs b/crates/vite_install/src/config.rs deleted file mode 100644 index 0ad115e8..00000000 --- a/crates/vite_install/src/config.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::{env, sync::LazyLock}; - -use directories::BaseDirs; -use vite_error::Error; -use vite_path::{AbsolutePathBuf, current_dir}; - -pub static NPM_REGISTRY: LazyLock = LazyLock::new(|| { - env::var("npm_config_registry") - .or_else(|_| env::var("NPM_CONFIG_REGISTRY")) - .unwrap_or_else(|_| "https://registry.npmjs.org".into()) -}); - -/// Get the tgz url of a npm package -pub fn get_npm_package_tgz_url(name: &str, version: &str) -> String { - // convert `@scope/name` to `name` - let filename = name.split('/').next_back().unwrap_or(name); - format!("{}/{}/-/{}-{}.tgz", NPM_REGISTRY.clone(), name, filename, version) -} - -pub fn get_npm_package_version_url(name: &str, version_or_tag: &str) -> String { - format!("{}/{}/{}", NPM_REGISTRY.clone(), name, version_or_tag) -} - -/// Cache directory -/// -/// It will use the cache directory of the operating system if available, -/// otherwise it will use the current directory. -pub fn get_cache_dir() -> Result { - let cache_dir = match BaseDirs::new() { - Some(dirs) => AbsolutePathBuf::new(dirs.cache_dir().to_path_buf()).unwrap(), - None => current_dir()?.join(".cache"), - }; - Ok(cache_dir.join("vite")) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_npm_registry() { - assert_eq!(NPM_REGISTRY.clone(), "https://registry.npmjs.org"); - } - - #[test] - fn test_npm_tgz_url() { - assert_eq!( - get_npm_package_tgz_url("vite", "7.1.3"), - "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz" - ); - assert_eq!( - get_npm_package_tgz_url("@vitejs/release-scripts", "1.6.0"), - "https://registry.npmjs.org/@vitejs/release-scripts/-/release-scripts-1.6.0.tgz" - ); - } - - #[test] - fn test_get_cache_dir() { - let cache_dir = get_cache_dir().unwrap(); - assert!(cache_dir.ends_with("vite")); - } -} diff --git a/crates/vite_install/src/lib.rs b/crates/vite_install/src/lib.rs deleted file mode 100644 index 837a77b3..00000000 --- a/crates/vite_install/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod commands; -mod config; -pub mod package_manager; -mod request; -mod shim; - -pub use package_manager::{PackageManager, PackageManagerType}; diff --git a/crates/vite_install/src/main.rs b/crates/vite_install/src/main.rs deleted file mode 100644 index fce6d014..00000000 --- a/crates/vite_install/src/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -use vite_error::Error; -use vite_install::PackageManager; -use vite_path::current_dir; - -#[tokio::main] -async fn main() -> Result<(), Error> { - let current_dir = current_dir()?; - let package_manager = PackageManager::builder(¤t_dir).build().await?; - println!("Package manager: {package_manager:#?} for {current_dir:?}"); - - let resolve_command = package_manager.resolve_install_command(&vec![]); - println!("Resolve command: {resolve_command:#?}"); - - Ok(()) -} diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs deleted file mode 100644 index d667598f..00000000 --- a/crates/vite_install/src/package_manager.rs +++ /dev/null @@ -1,1857 +0,0 @@ -use std::{ - collections::HashMap, - env, fmt, - fs::{self, File}, - io::BufReader, - path::Path, - process::{ExitStatus, Stdio}, -}; - -use semver::{Version, VersionReq}; -use serde::{Deserialize, Serialize}; -use tokio::{fs::remove_dir_all, process::Command}; -use vite_error::Error; -use vite_path::{AbsolutePath, AbsolutePathBuf}; -use vite_str::Str; -#[cfg(test)] -use vite_workspace::find_package_root; -use vite_workspace::{WorkspaceFile, WorkspaceRoot, find_workspace_root}; - -use crate::{ - config::{get_cache_dir, get_npm_package_tgz_url, get_npm_package_version_url}, - request::{HttpClient, download_and_extract_tgz_with_hash}, - shim, -}; - -#[derive(Serialize, Deserialize, Clone, Default)] -#[serde(rename_all = "camelCase")] -struct PackageJson { - #[serde(default)] - pub version: Str, - #[serde(default)] - pub package_manager: Str, -} - -/// The package manager type. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageManagerType { - Pnpm, - Yarn, - Npm, -} - -impl fmt::Display for PackageManagerType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Pnpm => write!(f, "pnpm"), - Self::Yarn => write!(f, "yarn"), - Self::Npm => write!(f, "npm"), - } - } -} - -// TODO(@fengmk2): should move ResolveCommandResult to vite-common crate -#[derive(Debug)] -pub struct ResolveCommandResult { - pub bin_path: String, - pub args: Vec, - pub envs: HashMap, -} - -/// The package manager. -/// Use `PackageManager::builder()` to create a package manager. -/// Then use `PackageManager::resolve_command()` to resolve the command result. -#[derive(Debug)] -pub struct PackageManager { - pub client: PackageManagerType, - pub package_name: Str, - pub version: Str, - pub hash: Option, - pub bin_name: Str, - pub workspace_root: AbsolutePathBuf, - pub install_dir: AbsolutePathBuf, -} - -#[derive(Debug)] -pub struct PackageManagerBuilder { - client_override: Option, - cwd: AbsolutePathBuf, -} - -impl PackageManagerBuilder { - pub fn new(cwd: impl AsRef) -> Self { - Self { client_override: None, cwd: cwd.as_ref().to_absolute_path_buf() } - } - - #[must_use] - pub const fn package_manager_type(mut self, package_manager_type: PackageManagerType) -> Self { - self.client_override = Some(package_manager_type); - self - } - - /// Build the package manager. - /// Detect the package manager from the current working directory. - pub async fn build(self) -> Result { - let workspace_root = find_workspace_root(&self.cwd)?; - let (package_manager_type, mut version, mut hash) = - get_package_manager_type_and_version(&workspace_root, self.client_override)?; - - let mut package_name = package_manager_type.to_string(); - let mut should_update_package_manager_field = false; - - if version == "latest" { - version = get_latest_version(package_manager_type).await?; - should_update_package_manager_field = true; - hash = None; // Reset hash when fetching latest since hash is version-specific - } - - // handle yarn >= 2.0.0 to use `@yarnpkg/cli-dist` as package name - // @see https://github.com/nodejs/corepack/blob/main/config.json#L135 - if matches!(package_manager_type, PackageManagerType::Yarn) { - let version_req = VersionReq::parse(">=2.0.0")?; - if version_req.matches(&Version::parse(&version)?) { - package_name = "@yarnpkg/cli-dist".to_string(); - } - } - - // only download the package manager if it's not already downloaded - let install_dir = download_package_manager( - package_manager_type, - &package_name, - &version, - hash.as_deref(), - ) - .await?; - - if should_update_package_manager_field { - // auto set `packageManager` field in package.json - let package_json_path = workspace_root.path.join("package.json"); - set_package_manager_field(&package_json_path, package_manager_type, &version).await?; - } - - Ok(PackageManager { - client: package_manager_type, - package_name: package_name.into(), - version, - hash, - bin_name: package_manager_type.to_string().into(), - workspace_root: workspace_root.path.to_absolute_path_buf(), - install_dir, - }) - } -} - -impl PackageManager { - pub fn builder(cwd: impl AsRef) -> PackageManagerBuilder { - PackageManagerBuilder::new(cwd) - } - - #[must_use] - pub fn get_bin_prefix(&self) -> AbsolutePathBuf { - self.install_dir.join("bin") - } - - #[must_use] - pub fn get_fingerprint_ignores(&self) -> Vec { - let mut ignores: Vec = vec![ - // ignore all files by default - "**/*".into(), - // keep all package.json files except under node_modules - "!**/package.json".into(), - "!**/.npmrc".into(), - ]; - match self.client { - PackageManagerType::Pnpm => { - ignores.push("!**/pnpm-workspace.yaml".into()); - ignores.push("!**/pnpm-lock.yaml".into()); - // https://pnpm.io/pnpmfile - ignores.push("!**/.pnpmfile.cjs".into()); - ignores.push("!**/pnpmfile.cjs".into()); - // pnpm support Plug'n'Play https://pnpm.io/blog/2020/10/17/node-modules-configuration-options-with-pnpm#plugnplay-the-strictest-configuration - ignores.push("!**/.pnp.cjs".into()); - } - PackageManagerType::Yarn => { - ignores.push("!**/.yarnrc".into()); // yarn 1.x - ignores.push("!**/.yarnrc.yml".into()); // yarn 2.x - ignores.push("!**/yarn.config.cjs".into()); // yarn 2.x - ignores.push("!**/yarn.lock".into()); - // .yarn/patches, .yarn/releases - ignores.push("!**/.yarn/**/*".into()); - // .pnp.cjs https://yarnpkg.com/features/pnp - ignores.push("!**/.pnp.cjs".into()); - } - PackageManagerType::Npm => { - ignores.push("!**/package-lock.json".into()); - ignores.push("!**/npm-shrinkwrap.json".into()); - } - } - // ignore all files under node_modules - // e.g. node_modules/mqtt/package.json - ignores.push("**/node_modules/**/*".into()); - // keep the node_modules directory - ignores.push("!**/node_modules".into()); - // keep the scoped directory - ignores.push("!**/node_modules/@*".into()); - // ignore all patterns under nested node_modules - // e.g. node_modules/mqtt/node_modules/mqtt-packet/node_modules - ignores.push("**/node_modules/**/node_modules/**".into()); - - ignores - } -} - -/// Get the package manager name, version and optional hash from the workspace root. -fn get_package_manager_type_and_version( - workspace_root: &WorkspaceRoot, - default: Option, -) -> Result<(PackageManagerType, Str, Option), Error> { - // check packageManager field in package.json - let package_json_path = workspace_root.path.join("package.json"); - if let Some(file) = open_exists_file(&package_json_path)? { - let package_json: PackageJson = serde_json::from_reader(BufReader::new(&file))?; - if !package_json.package_manager.is_empty() - && let Some((name, version_with_hash)) = package_json.package_manager.split_once('@') - { - // Parse version and optional hash (format: version+sha512.hash) - let (version, hash) = if let Some((ver, hash_part)) = version_with_hash.split_once('+') - { - (ver, Some(hash_part.into())) - } else { - (version_with_hash, None) - }; - - // check if the version is a valid semver - semver::Version::parse(version).map_err(|_| Error::PackageManagerVersionInvalid { - name: name.into(), - version: version.into(), - package_json_path: package_json_path.to_absolute_path_buf(), - })?; - match name { - "pnpm" => return Ok((PackageManagerType::Pnpm, version.into(), hash)), - "yarn" => return Ok((PackageManagerType::Yarn, version.into(), hash)), - "npm" => return Ok((PackageManagerType::Npm, version.into(), hash)), - _ => return Err(Error::UnsupportedPackageManager(name.into())), - } - } - } - - // TODO(@fengmk2): check devEngines.packageManager field in package.json - - let version = Str::from("latest"); - // if pnpm-workspace.yaml exists, use pnpm@latest - if matches!(workspace_root.workspace_file, WorkspaceFile::PnpmWorkspaceYaml(_)) { - return Ok((PackageManagerType::Pnpm, version, None)); - } - - // if pnpm-lock.yaml exists, use pnpm@latest - let pnpm_lock_yaml_path = workspace_root.path.join("pnpm-lock.yaml"); - if is_exists_file(&pnpm_lock_yaml_path)? { - return Ok((PackageManagerType::Pnpm, version, None)); - } - - // if yarn.lock or .yarnrc.yml exists, use yarn@latest - let yarn_lock_path = workspace_root.path.join("yarn.lock"); - let yarnrc_yml_path = workspace_root.path.join(".yarnrc.yml"); - if is_exists_file(&yarn_lock_path)? || is_exists_file(&yarnrc_yml_path)? { - return Ok((PackageManagerType::Yarn, version, None)); - } - - // if package-lock.json exists, use npm@latest - let package_lock_json_path = workspace_root.path.join("package-lock.json"); - if is_exists_file(&package_lock_json_path)? { - return Ok((PackageManagerType::Npm, version, None)); - } - - // if .pnpmfile.cjs exists, use pnpm@latest - let pnpmfile_cjs_path = workspace_root.path.join(".pnpmfile.cjs"); - if is_exists_file(&pnpmfile_cjs_path)? { - return Ok((PackageManagerType::Pnpm, version, None)); - } - // if legacy pnpmfile.cjs exists, use pnpm@latest - // https://newreleases.io/project/npm/pnpm/release/6.0.0 - let legacy_pnpmfile_cjs_path = workspace_root.path.join("pnpmfile.cjs"); - if is_exists_file(&legacy_pnpmfile_cjs_path)? { - return Ok((PackageManagerType::Pnpm, version, None)); - } - - // if yarn.config.cjs exists, use yarn@latest (yarn 2.0+) - let yarn_config_cjs_path = workspace_root.path.join("yarn.config.cjs"); - if is_exists_file(&yarn_config_cjs_path)? { - return Ok((PackageManagerType::Yarn, version, None)); - } - - // if default is specified, use it - if let Some(default) = default { - return Ok((default, version, None)); - } - - // unrecognized package manager, let user specify the package manager - Err(Error::UnrecognizedPackageManager) -} - -/// Open the file if it exists, otherwise return None. -fn open_exists_file(path: impl AsRef) -> Result, Error> { - match File::open(path) { - Ok(file) => Ok(Some(file)), - // if the file does not exist, return None - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(e.into()), - } -} - -/// Check if the file exists. -fn is_exists_file(path: impl AsRef) -> Result { - match fs::metadata(path) { - Ok(metadata) => Ok(metadata.is_file()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(e) => Err(e.into()), - } -} - -async fn get_latest_version(package_manager_type: PackageManagerType) -> Result { - let package_name = if matches!(package_manager_type, PackageManagerType::Yarn) { - // yarn latest version should use `@yarnpkg/cli-dist` as package name - "@yarnpkg/cli-dist".to_string() - } else { - package_manager_type.to_string() - }; - let url = get_npm_package_version_url(&package_name, "latest"); - let package_json: PackageJson = HttpClient::new().get_json(&url).await?; - Ok(package_json.version) -} - -/// Download the package manager and extract it to the cache directory. -/// Return the install directory, e.g. $`CACHE_DIR/vite/package_manager/pnpm/10.0.0/pnpm` -async fn download_package_manager( - package_manager_type: PackageManagerType, - package_name: &str, - version: &str, - expected_hash: Option<&str>, -) -> Result { - let tgz_url = get_npm_package_tgz_url(package_name, version); - let cache_dir = get_cache_dir()?; - let bin_name = package_manager_type.to_string(); - // $CACHE_DIR/vite/package_manager/pnpm/10.0.0 - let target_dir = cache_dir.join(format!("package_manager/{bin_name}/{version}")); - let install_dir = target_dir.join(&bin_name); - - // If all shims are already exists, return the target directory - // $CACHE_DIR/vite/package_manager/pnpm/10.0.0/pnpm/bin/(pnpm|pnpm.cmd|pnpm.ps1) - let bin_prefix = install_dir.join("bin"); - let bin_file = bin_prefix.join(&bin_name); - if is_exists_file(&bin_file)? - && is_exists_file(bin_file.with_extension("cmd"))? - && is_exists_file(bin_file.with_extension("ps1"))? - { - return Ok(install_dir); - } - - // $CACHE_DIR/vite/package_manager/pnpm/{tmp_name} - // Use tempfile::TempDir for robust temporary directory creation - let parent_dir = target_dir.parent().unwrap(); - tokio::fs::create_dir_all(parent_dir).await?; - let target_dir_tmp = tempfile::tempdir_in(parent_dir)?.path().to_path_buf(); - - download_and_extract_tgz_with_hash(&tgz_url, &target_dir_tmp, expected_hash).await.map_err( - |err| { - // status 404 means the version is not found, convert to PackageManagerVersionNotFound error - if let Error::Reqwest(e) = &err - && let Some(status) = e.status() - && status == reqwest::StatusCode::NOT_FOUND - { - Error::PackageManagerVersionNotFound { - name: package_manager_type.to_string().into(), - version: version.into(), - url: tgz_url.into(), - } - } else { - err - } - }, - )?; - - // rename $target_dir_tmp/package to $target_dir_tmp/{bin_name} - tracing::debug!("Rename package dir to {}", bin_name); - tokio::fs::rename(&target_dir_tmp.join("package"), &target_dir_tmp.join(&bin_name)).await?; - - // check bin_file again, for the concurrent download cases - if is_exists_file(&bin_file)? { - tracing::debug!("bin_file already exists, skip rename"); - return Ok(install_dir); - } - - // rename $target_dir_tmp to $target_dir - tracing::debug!("Rename {:?} to {:?}", target_dir_tmp, target_dir); - remove_dir_all_force(&target_dir).await?; - tokio::fs::rename(&target_dir_tmp, &target_dir).await?; - - // create shim file - tracing::debug!("Create shim files for {}", bin_name); - create_shim_files(package_manager_type, &bin_prefix).await?; - - Ok(install_dir) -} - -/// Remove the directory and all its contents. -/// Ignore the error if the directory is not found. -async fn remove_dir_all_force(path: impl AsRef) -> Result<(), std::io::Error> { - match remove_dir_all(path).await { - Ok(()) => Ok(()), - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Ok(()) - } else { - Err(e) - } - } - } -} - -/// Create shim files for the package manager. -/// -/// Will automatically create `{cli_name}.cjs`, `{cli_name}.cmd`, `{cli_name}.ps1` files for the package manager. -/// Example: -/// - $`bin_prefix/pnpm` -> $`bin_prefix/pnpm.cjs` -/// - $`bin_prefix/pnpm.cmd` -> $`bin_prefix/pnpm.cjs` -/// - $`bin_prefix/pnpm.ps1` -> $`bin_prefix/pnpm.cjs` -/// - $`bin_prefix/pnpx` -> $`bin_prefix/pnpx.cjs` -/// - $`bin_prefix/pnpx.cmd` -> $`bin_prefix/pnpx.cjs` -/// - $`bin_prefix/pnpx.ps1` -> $`bin_prefix/pnpx.cjs` -async fn create_shim_files( - package_manager_type: PackageManagerType, - bin_prefix: impl AsRef, -) -> Result<(), Error> { - let mut bin_names: Vec<(&str, &str)> = Vec::new(); - - match package_manager_type { - PackageManagerType::Pnpm => { - bin_names.push(("pnpm", "pnpm")); - bin_names.push(("pnpx", "pnpx")); - } - PackageManagerType::Yarn => { - // yarn don't have the `npx` like cli, so we don't need to create shim files for it - bin_names.push(("yarn", "yarn")); - // but it has alias `yarnpkg` - bin_names.push(("yarnpkg", "yarn")); - } - PackageManagerType::Npm => { - // npm has two cli: bin/npm-cli.js and bin/npx-cli.js - bin_names.push(("npm", "npm-cli")); - bin_names.push(("npx", "npx-cli")); - } - } - - let bin_prefix = bin_prefix.as_ref(); - for (bin_name, js_bin_basename) in bin_names { - // try .cjs first - let mut js_bin_name = format!("{js_bin_basename}.cjs"); - if !is_exists_file(bin_prefix.join(&js_bin_name))? { - // fallback to .js - js_bin_name = format!("{js_bin_basename}.js"); - if !is_exists_file(bin_prefix.join(&js_bin_name))? { - continue; - } - } - - let source_file = bin_prefix.join(js_bin_name); - let to_bin = bin_prefix.join(bin_name); - shim::write_shims(&source_file, &to_bin).await?; - } - Ok(()) -} - -async fn set_package_manager_field( - package_json_path: impl AsRef, - package_manager_type: PackageManagerType, - version: &str, -) -> Result<(), Error> { - let package_json_path = package_json_path.as_ref(); - let package_manager_value = format!("{package_manager_type}@{version}"); - let mut package_json = if is_exists_file(package_json_path)? { - let content = tokio::fs::read(&package_json_path).await?; - serde_json::from_slice(&content)? - } else { - serde_json::json!({}) - }; - // use IndexMap to preserve the order of the fields - if let Some(package_json) = package_json.as_object_mut() { - package_json.insert("packageManager".into(), serde_json::json!(package_manager_value)); - } - let json_string = serde_json::to_string_pretty(&package_json)?; - tokio::fs::write(&package_json_path, json_string).await?; - tracing::debug!( - "set_package_manager_field: {:?} to {:?}", - package_json_path, - package_manager_value - ); - Ok(()) -} - -pub(crate) fn format_path_env(bin_prefix: impl AsRef) -> String { - let mut paths = env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect::>(); - paths.insert(0, bin_prefix.as_ref().to_path_buf()); - env::join_paths(paths).unwrap().to_string_lossy().to_string() -} - -#[cfg(unix)] -fn fix_stdio_streams() { - // libuv may mark stdin/stdout/stderr as close-on-exec, which interferes with Rust's subprocess spawning. - // As a workaround, we clear the FD_CLOEXEC flag on these file descriptors to prevent them from being closed when spawning child processes. - // - // For details see https://github.com/libuv/libuv/issues/2062 - // Fixed by reference from https://github.com/electron/electron/pull/15555 - - use std::os::fd::BorrowedFd; - - use nix::{ - fcntl::{FcntlArg, FdFlag, fcntl}, - libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, - }; - - // Safe function to clear FD_CLOEXEC flag - fn clear_cloexec(fd: BorrowedFd<'_>) { - // Borrow RawFd as BorrowedFd to satisfy AsFd constraint - if let Ok(flags) = fcntl(fd, FcntlArg::F_GETFD) { - let mut fd_flags = FdFlag::from_bits_retain(flags); - if fd_flags.contains(FdFlag::FD_CLOEXEC) { - fd_flags.remove(FdFlag::FD_CLOEXEC); - // Ignore errors: some fd may be closed - let _ = fcntl(fd, FcntlArg::F_SETFD(fd_flags)); - } - } - } - - // Clear FD_CLOEXEC on stdin, stdout, stderr - clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }); - clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDOUT_FILENO) }); - clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDERR_FILENO) }); -} - -// TODO: should move to vite-command crate later -/// Run a command with the given bin name, arguments, environment variables, and current working directory. -/// -/// # Arguments -/// -/// * `bin_name`: The name of the binary to run. -/// * `args`: The arguments to pass to the binary. -/// * `envs`: The custom environment variables to set for the command, will be merged with the system environment variables. -/// * `cwd`: The current working directory for the command. -/// -/// # Returns -/// -/// Returns the exit status of the command. -pub(crate) async fn run_command( - bin_name: &str, - args: &[String], - envs: &HashMap, - cwd: impl AsRef, -) -> Result { - println!("Running: {} {}", bin_name, args.join(" ")); - - let mut cmd = Command::new(bin_name); - cmd.args(args) - .envs(envs) - .current_dir(cwd.as_ref()) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()); - - // fix stdio streams on unix - #[cfg(unix)] - unsafe { - cmd.pre_exec(|| { - fix_stdio_streams(); - Ok(()) - }); - } - - let status = cmd.status().await?; - Ok(status) -} - -#[cfg(test)] -mod tests { - use std::fs; - - use tempfile::{TempDir, tempdir}; - - use super::*; - - fn create_temp_dir() -> TempDir { - tempdir().expect("Failed to create temp directory") - } - - fn create_package_json(dir: &AbsolutePath, content: &str) { - fs::write(dir.join("package.json"), content).expect("Failed to write package.json"); - } - - fn create_pnpm_workspace_yaml(dir: &AbsolutePath, content: &str) { - fs::write(dir.join("pnpm-workspace.yaml"), content) - .expect("Failed to write pnpm-workspace.yaml"); - } - - #[test] - fn test_find_package_root() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let nested_dir = temp_dir_path.join("a").join("b").join("c"); - fs::create_dir_all(&nested_dir).unwrap(); - - // Create package.json in a/b - let package_dir = temp_dir_path.join("a").join("b"); - File::create(package_dir.join("package.json")).unwrap(); - - // Should find package.json in parent directory - let found = find_package_root(&nested_dir); - let package_root = found.unwrap(); - assert_eq!(package_root.path, package_dir); - - // Should return the same directory if package.json is there - let found = find_package_root(&package_dir); - let package_root = found.unwrap(); - assert_eq!(package_root.path, package_dir); - - // Should return PackageJsonNotFound error if no package.json found - let root_dir = temp_dir_path.join("x").join("y"); - fs::create_dir_all(&root_dir).unwrap(); - let found = find_package_root(&root_dir); - let err = found.unwrap_err(); - assert!(matches!(err, vite_workspace::Error::PackageJsonNotFound(_))); - } - - #[test] - fn test_find_workspace_root_with_pnpm() { - let temp_dir = create_temp_dir(); - - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let nested_dir = temp_dir_path.join("packages").join("app"); - fs::create_dir_all(&nested_dir).unwrap(); - - // Create pnpm-workspace.yaml at root - File::create(temp_dir_path.join("pnpm-workspace.yaml")).unwrap(); - - // Should find workspace root - let found = find_workspace_root(&nested_dir).unwrap(); - assert_eq!(found.path, temp_dir_path); - assert!(matches!(found.workspace_file, WorkspaceFile::PnpmWorkspaceYaml(_))); - } - - #[test] - fn test_find_workspace_root_with_npm_workspaces() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let nested_dir = temp_dir_path.join("packages").join("app"); - fs::create_dir_all(&nested_dir).unwrap(); - - // Create package.json with workspaces field - let package_json = r#"{"workspaces": ["packages/*"]}"#; - fs::write(temp_dir_path.join("package.json"), package_json).unwrap(); - - // Should find workspace root - let found = find_workspace_root(&temp_dir_path).unwrap(); - assert_eq!(found.path, temp_dir_path); - assert!(matches!(found.workspace_file, WorkspaceFile::NpmWorkspaceJson(_))); - } - - #[test] - fn test_find_workspace_root_fallback_to_package_root() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let nested_dir = temp_dir_path.join("src"); - fs::create_dir_all(&nested_dir).unwrap(); - - // Create package.json without workspaces field - let package_json = r#"{"name": "test"}"#; - fs::write(temp_dir_path.join("package.json"), package_json).unwrap(); - - // Should fallback to package root - let found = find_workspace_root(&nested_dir).unwrap(); - assert_eq!(found.path, temp_dir_path); - assert!(matches!(found.workspace_file, WorkspaceFile::NonWorkspacePackage(_))); - let package_root = find_package_root(&temp_dir_path).unwrap(); - // equal to workspace root - assert_eq!(package_root.path, found.path); - } - - #[test] - fn test_find_workspace_root_with_package_json_not_found() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let nested_dir = temp_dir_path.join("src"); - fs::create_dir_all(&nested_dir).unwrap(); - - // Should return PackageJsonNotFound error if no package.json found - let found = find_workspace_root(&nested_dir); - let err = found.unwrap_err(); - assert!(matches!(err, vite_workspace::Error::PackageJsonNotFound(_))); - } - - #[test] - fn test_find_package_root_with_package_json_in_current_dir() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = find_package_root(&temp_dir_path).unwrap(); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_package_root_with_package_json_in_parent_dir() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let sub_dir = temp_dir_path.join("subdir"); - fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); - - let result = find_package_root(&sub_dir).unwrap(); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_package_root_with_package_json_in_grandparent_dir() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let sub_dir = temp_dir_path.join("subdir").join("nested"); - fs::create_dir_all(&sub_dir).expect("Failed to create nested directories"); - - let result = find_package_root(&sub_dir).unwrap(); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_with_pnpm_workspace_yaml() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let result = find_workspace_root(&temp_dir_path).expect("Should find workspace root"); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_with_pnpm_workspace_yaml_in_parent_dir() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let sub_dir = temp_dir_path.join("subdir"); - fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); - - let result = find_workspace_root(&sub_dir).expect("Should find workspace root"); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_with_package_json_workspaces() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-workspace", "workspaces": ["packages/*"]}"#; - create_package_json(&temp_dir_path, package_content); - - let result = find_workspace_root(&temp_dir_path).unwrap(); - assert_eq!(result.path, temp_dir_path); - assert!(matches!(result.workspace_file, WorkspaceFile::NpmWorkspaceJson(_))); - } - - #[test] - fn test_find_workspace_root_with_package_json_workspaces_in_parent_dir() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-workspace", "workspaces": ["packages/*"]}"#; - create_package_json(&temp_dir_path, package_content); - - let sub_dir = temp_dir_path.join("subdir"); - fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); - - let result = find_workspace_root(&sub_dir).unwrap(); - assert_eq!(result.path, temp_dir_path); - assert!(matches!(result.workspace_file, WorkspaceFile::NpmWorkspaceJson(_))); - } - - #[test] - fn test_find_workspace_root_prioritizes_pnpm_workspace_over_package_json() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Create package.json with workspaces first - let package_content = r#"{"name": "test-workspace", "workspaces": ["packages/*"]}"#; - create_package_json(&temp_dir_path, package_content); - - // Then create pnpm-workspace.yaml (should take precedence) - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let result = find_workspace_root(&temp_dir_path).expect("Should find workspace root"); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_falls_back_to_package_root_when_no_workspace_found() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let sub_dir = temp_dir_path.join("subdir"); - fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); - - let result = find_workspace_root(&sub_dir).expect("Should fall back to package root"); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_with_nested_structure() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let nested_dir = temp_dir_path.join("packages").join("app").join("src"); - fs::create_dir_all(&nested_dir).expect("Failed to create nested directories"); - - let result = find_workspace_root(&nested_dir).expect("Should find workspace root"); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_without_workspace_files_returns_package_root() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = find_workspace_root(&temp_dir_path).expect("Should return package root"); - assert_eq!(result.path, temp_dir_path); - } - - #[test] - fn test_find_workspace_root_with_invalid_package_json_handles_error() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let invalid_package_content = "{ invalid json content"; - create_package_json(&temp_dir_path, invalid_package_content); - - let result = find_workspace_root(&temp_dir_path); - assert!(result.is_err()); - } - - #[test] - fn test_find_workspace_root_with_mixed_structure() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - // Create a package.json without workspaces - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create a subdirectory with its own package.json - let sub_dir = temp_dir_path.join("subdir"); - fs::create_dir(&sub_dir).expect("Failed to create subdirectory"); - let sub_package_content = r#"{"name": "sub-package"}"#; - create_package_json(&sub_dir, sub_package_content); - - // Should find the subdirectory package.json since find_package_root searches upward from original_cwd - let workspace_root = - find_workspace_root(&sub_dir).expect("Should find subdirectory package"); - assert_eq!(workspace_root.path, sub_dir); - assert!(matches!(workspace_root.workspace_file, WorkspaceFile::NonWorkspacePackage(_))); - } - - #[tokio::test] - async fn test_detect_package_manager_with_pnpm_workspace_yaml() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let result = - PackageManager::builder(temp_dir_path).build().await.expect("Should detect pnpm"); - assert_eq!(result.bin_name, "pnpm"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_pnpm_lock_yaml() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "version": "1.0.0"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create pnpm-lock.yaml - fs::write(temp_dir_path.join("pnpm-lock.yaml"), "lockfileVersion: '6.0'") - .expect("Failed to write pnpm-lock.yaml"); - - let result = - PackageManager::builder(temp_dir_path).build().await.expect("Should detect pnpm"); - assert_eq!(result.bin_name, "pnpm"); - - // check if the package.json file has the `packageManager` field - let package_json_path = temp_dir.path().join("package.json"); - let package_json: serde_json::Value = - serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap(); - println!("package_json: {package_json:?}"); - assert!(package_json["packageManager"].as_str().unwrap().starts_with("pnpm@")); - // keep other fields - assert_eq!(package_json["version"].as_str().unwrap(), "1.0.0"); - assert_eq!(package_json["name"].as_str().unwrap(), "test-package"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_yarn_lock() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create yarn.lock - fs::write(temp_dir_path.join("yarn.lock"), "# yarn lockfile v1") - .expect("Failed to write yarn.lock"); - - let result = PackageManager::builder(temp_dir_path.to_absolute_path_buf()) - .build() - .await - .expect("Should detect yarn"); - assert_eq!(result.bin_name, "yarn"); - assert_eq!(result.workspace_root, temp_dir_path); - assert!( - result.get_bin_prefix().ends_with("yarn/bin"), - "bin_prefix should end with yarn/bin, but got {:?}", - result.get_bin_prefix() - ); - // package.json should have the `packageManager` field - let package_json_path = temp_dir_path.join("package.json"); - let package_json: serde_json::Value = - serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap(); - println!("package_json: {package_json:?}"); - assert!(package_json["packageManager"].as_str().unwrap().starts_with("yarn@")); - // keep other fields - assert_eq!(package_json["name"].as_str().unwrap(), "test-package"); - } - - #[tokio::test] - #[cfg(not(windows))] // FIXME - async fn test_detect_package_manager_with_package_lock_json() { - use std::process::Command; - - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create package-lock.json - fs::write(temp_dir_path.join("package-lock.json"), r#"{"lockfileVersion": 2}"#) - .expect("Failed to write package-lock.json"); - - let result = - PackageManager::builder(temp_dir_path).build().await.expect("Should detect npm"); - assert_eq!(result.bin_name, "npm"); - - // check shim files - let bin_prefix = result.get_bin_prefix(); - assert!(is_exists_file(bin_prefix.join("npm")).unwrap()); - assert!(is_exists_file(bin_prefix.join("npm.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("npm.ps1")).unwrap()); - assert!(is_exists_file(bin_prefix.join("npx")).unwrap()); - assert!(is_exists_file(bin_prefix.join("npx.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("npx.ps1")).unwrap()); - - // run npm --version - let mut paths = - env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect::>(); - paths.insert(0, bin_prefix.into_path_buf()); - let output = Command::new("npm") - .arg("--version") - .env("PATH", env::join_paths(&paths).unwrap()) - .output() - .expect("Failed to run npm"); - assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); - // println!("npm --version: {:?}", String::from_utf8_lossy(&output.stdout)); - - // run npx --version - let output = Command::new("npx") - .arg("--version") - .env("PATH", env::join_paths(&paths).unwrap()) - .output() - .expect("Failed to run npx"); - assert!(output.status.success(), "stderr: {}", String::from_utf8_lossy(&output.stderr)); - } - - #[tokio::test] - #[cfg(not(windows))] // FIXME - async fn test_detect_package_manager_with_package_manager_field() { - use std::process::Command; - - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "pnpm@8.15.0"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect pnpm with version"); - assert_eq!(result.bin_name, "pnpm"); - - // check shim files - let bin_prefix = result.get_bin_prefix(); - assert!(is_exists_file(bin_prefix.join("pnpm.cjs")).unwrap()); - assert!(is_exists_file(bin_prefix.join("pnpm.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("pnpm.ps1")).unwrap()); - assert!(is_exists_file(bin_prefix.join("pnpx.cjs")).unwrap()); - assert!(is_exists_file(bin_prefix.join("pnpx.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("pnpx.ps1")).unwrap()); - - // run pnpm --version - let mut paths = - env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect::>(); - paths.insert(0, bin_prefix.into_path_buf()); - let output = Command::new("pnpm") - .arg("--version") - .env("PATH", env::join_paths(paths).unwrap()) - .output() - .expect("Failed to run pnpm"); - // println!("pnpm --version: {:?}", output); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "8.15.0"); - } - - #[tokio::test] - async fn test_parse_package_manager_with_hash() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Test with sha512 hash - let package_content = r#"{"name": "test-package", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"}"#; - create_package_json(&temp_dir_path, package_content); - - let workspace_root = find_workspace_root(&temp_dir_path).unwrap(); - let (pm_type, version, hash) = - get_package_manager_type_and_version(&workspace_root, None).unwrap(); - - assert_eq!(pm_type, PackageManagerType::Yarn); - assert_eq!(version, "1.22.22"); - assert!(hash.is_some()); - assert_eq!( - hash.unwrap(), - "sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" - ); - } - - #[tokio::test] - async fn test_parse_package_manager_with_sha1_hash() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Test with sha1 hash - let package_content = r#"{"name": "test-package", "packageManager": "npm@10.5.0+sha1.abcd1234567890abcdef1234567890abcdef1234"}"#; - create_package_json(&temp_dir_path, package_content); - - let workspace_root = find_workspace_root(&temp_dir_path).unwrap(); - let (pm_type, version, hash) = - get_package_manager_type_and_version(&workspace_root, None).unwrap(); - - assert_eq!(pm_type, PackageManagerType::Npm); - assert_eq!(version, "10.5.0"); - assert!(hash.is_some()); - assert_eq!(hash.unwrap(), "sha1.abcd1234567890abcdef1234567890abcdef1234"); - } - - #[tokio::test] - async fn test_parse_package_manager_with_sha224_hash() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Test with sha224 hash - let package_content = r#"{"name": "test-package", "packageManager": "pnpm@8.15.0+sha224.1234567890abcdef1234567890abcdef1234567890abcdef12345678"}"#; - create_package_json(&temp_dir_path, package_content); - - let workspace_root = find_workspace_root(&temp_dir_path).unwrap(); - let (pm_type, version, hash) = - get_package_manager_type_and_version(&workspace_root, None).unwrap(); - - assert_eq!(pm_type, PackageManagerType::Pnpm); - assert_eq!(version, "8.15.0"); - assert!(hash.is_some()); - assert_eq!( - hash.unwrap(), - "sha224.1234567890abcdef1234567890abcdef1234567890abcdef12345678" - ); - } - - #[tokio::test] - async fn test_parse_package_manager_with_sha256_hash() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Test with sha256 hash - let package_content = r#"{"name": "test-package", "packageManager": "yarn@4.0.0+sha256.1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}"#; - create_package_json(&temp_dir_path, package_content); - - let workspace_root = find_workspace_root(&temp_dir_path).unwrap(); - let (pm_type, version, hash) = - get_package_manager_type_and_version(&workspace_root, None).unwrap(); - - assert_eq!(pm_type, PackageManagerType::Yarn); - assert_eq!(version, "4.0.0"); - assert!(hash.is_some()); - assert_eq!( - hash.unwrap(), - "sha256.1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - ); - } - - #[tokio::test] - async fn test_parse_package_manager_without_hash() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Test without hash - let package_content = r#"{"name": "test-package", "packageManager": "pnpm@8.15.0"}"#; - create_package_json(&temp_dir_path, package_content); - - let workspace_root = find_workspace_root(&temp_dir_path).unwrap(); - let (pm_type, version, hash) = - get_package_manager_type_and_version(&workspace_root, None).unwrap(); - - assert_eq!(pm_type, PackageManagerType::Pnpm); - assert_eq!(version, "8.15.0"); - assert!(hash.is_none()); - } - - #[tokio::test] - async fn test_download_success_package_manager_with_hash() { - use std::process::Command; - - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect yarn with version and hash"); - assert_eq!(result.bin_name, "yarn"); - - // check shim files - let bin_prefix = result.get_bin_prefix(); - assert!(is_exists_file(bin_prefix.join("yarn.js")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarn")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarn.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarn.ps1")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarnpkg")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarnpkg.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarnpkg.ps1")).unwrap()); - - // run pnpm --version - let mut paths = - env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect::>(); - paths.insert(0, bin_prefix.into_path_buf()); - let mut cmd = "yarn"; - if cfg!(windows) { - cmd = "yarn.cmd"; - } - let output = Command::new(cmd) - .arg("--version") - .env("PATH", env::join_paths(paths).unwrap()) - .output() - .expect("Failed to run yarn"); - // println!("pnpm --version: {:?}", output); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "1.22.22"); - } - - #[tokio::test] - async fn test_download_failed_package_manager_with_hash() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "yarn@1.22.21+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path).build().await; - assert!(result.is_err()); - // Check if it's the expected error type - if let Err(Error::HashMismatch { expected, actual }) = result { - assert_eq!( - expected, - "sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" - ); - assert_eq!( - actual, - "sha512.ca75da26c00327d26267ce33536e5790f18ebd53266796fbb664d2a4a5116308042dd8ee7003b276a20eace7d3c5561c3577bdd71bcb67071187af124779620a" - ); - } else { - panic!("Expected HashMismatch error"); - } - } - - #[tokio::test] - async fn test_download_success_package_manager_with_sha1_and_sha224() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "yarn@1.22.20+sha1.167c8ab8d9c8c3826d3725d9579aaea8b47a2b18"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect yarn with version and hash"); - assert_eq!(result.bin_name, "yarn"); - - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "pnpm@4.11.6+sha224.7783c4b01916b7a69e6ff05d328df6f83cb7f127e9c96be88739386d"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect pnpm with version and hash"); - assert_eq!(result.bin_name, "pnpm"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_yarn_package_manager_field() { - use std::process::Command; - - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "yarn@4.0.0"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path.clone()) - .build() - .await - .expect("Should detect yarn with version"); - assert_eq!(result.bin_name, "yarn"); - - assert_eq!(result.version, "4.0.0"); - assert_eq!(result.workspace_root, temp_dir_path); - assert!( - result.get_bin_prefix().ends_with("yarn/bin"), - "bin_prefix should end with yarn/bin, but got {:?}", - result.get_bin_prefix() - ); - - // check shim files - let bin_prefix = result.get_bin_prefix(); - assert!(is_exists_file(bin_prefix.join("yarn.js")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarn")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarn.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarn.ps1")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarnpkg")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarnpkg.cmd")).unwrap()); - assert!(is_exists_file(bin_prefix.join("yarnpkg.ps1")).unwrap()); - - // run yarn --version - let mut cmd = "yarn"; - if cfg!(windows) { - cmd = "yarn.cmd"; - } - let mut paths = - env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect::>(); - paths.insert(0, bin_prefix.into_path_buf()); - let output = Command::new(cmd) - .arg("--version") - .env("PATH", env::join_paths(paths).unwrap()) - .output() - .expect("Failed to run yarn"); - assert!(output.status.success()); - assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "4.0.0"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_npm_package_manager_field() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "npm@10.0.0"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect npm with version"); - assert_eq!(result.bin_name, "npm"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_invalid_package_manager_field() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "invalid@1.0.0"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path).build().await; - assert!(result.is_err()); - // Check if it's the expected error type - if let Err(Error::UnsupportedPackageManager(name)) = result { - assert_eq!(name, "invalid"); - } else { - panic!("Expected UnsupportedPackageManager error"); - } - } - - #[tokio::test] - async fn test_detect_package_manager_with_not_exists_version_in_package_manager_field() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = - r#"{"name": "test-package", "packageManager": "yarn@10000000000.0.0"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path).build().await; - assert!(result.is_err()); - println!("result: {result:?}"); - // Check if it's the expected error type - if let Err(Error::PackageManagerVersionNotFound { name, version, .. }) = result { - assert_eq!(name, "yarn"); - assert_eq!(version, "10000000000.0.0"); - } else { - panic!("Expected PackageManagerVersionNotFound error, got {result:?}"); - } - } - - #[tokio::test] - async fn test_detect_package_manager_with_invalid_semver() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = - r#"{"name": "test-package", "packageManager": "pnpm@invalid-version"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path).build().await; - println!("result: {result:?}"); - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_detect_package_manager_with_default_fallback() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path.clone()) - .package_manager_type(PackageManagerType::Yarn) - .build() - .await - .expect("Should use default"); - assert_eq!(result.bin_name, "yarn"); - // package.json should have the `packageManager` field - let package_json_path = temp_dir_path.join("package.json"); - let package_json: serde_json::Value = - serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap(); - // println!("package_json: {:?}", package_json); - assert!(package_json["packageManager"].as_str().unwrap().starts_with("yarn@")); - // keep other fields - assert_eq!(package_json["name"].as_str().unwrap(), "test-package"); - } - - #[tokio::test] - async fn test_detect_package_manager_without_any_indicators() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - let result = PackageManager::builder(temp_dir_path).build().await; - assert!(result.is_err()); - // Check if it's the expected error type - if matches!(result, Err(Error::UnrecognizedPackageManager)) { - // Expected error - } else { - panic!("Expected UnrecognizedPackageManager error"); - } - } - - #[tokio::test] - async fn test_detect_package_manager_prioritizes_package_manager_field_over_lock_files() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package", "packageManager": "yarn@4.0.0"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create pnpm-lock.yaml (should be ignored due to packageManager field) - fs::write(temp_dir_path.join("pnpm-lock.yaml"), "lockfileVersion: '6.0'") - .expect("Failed to write pnpm-lock.yaml"); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect yarn from packageManager field"); - assert_eq!(result.bin_name, "yarn"); - } - - #[tokio::test] - async fn test_detect_package_manager_prioritizes_pnpm_workspace_over_lock_files() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create yarn.lock (should be ignored due to pnpm-workspace.yaml) - fs::write(temp_dir_path.join("yarn.lock"), "# yarn lockfile v1") - .expect("Failed to write yarn.lock"); - - // Create pnpm-workspace.yaml (should take precedence) - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect pnpm from workspace file"); - assert_eq!(result.bin_name, "pnpm"); - } - - #[tokio::test] - async fn test_detect_package_manager_from_subdirectory() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let workspace_content = "packages:\n - 'packages/*'"; - create_pnpm_workspace_yaml(&temp_dir_path, workspace_content); - - let sub_dir = temp_dir_path.join("packages").join("app"); - fs::create_dir_all(&sub_dir).expect("Failed to create subdirectory"); - - let result = PackageManager::builder(sub_dir) - .build() - .await - .expect("Should detect pnpm from parent workspace"); - assert_eq!(result.bin_name, "pnpm"); - assert!(result.get_bin_prefix().ends_with("pnpm/bin")); - } - - #[tokio::test] - async fn test_download_package_manager() { - let result = - download_package_manager(PackageManagerType::Yarn, "@yarnpkg/cli-dist", "4.9.2", None) - .await; - assert!(result.is_ok()); - let target_dir = result.unwrap(); - println!("result: {target_dir:?}"); - assert!(is_exists_file(target_dir.join("bin/yarn")).unwrap()); - assert!(is_exists_file(target_dir.join("bin/yarn.cmd")).unwrap()); - - // again should skip download - let result = - download_package_manager(PackageManagerType::Yarn, "@yarnpkg/cli-dist", "4.9.2", None) - .await; - assert!(result.is_ok()); - let target_dir = result.unwrap(); - assert!(is_exists_file(target_dir.join("bin/yarn")).unwrap()); - assert!(is_exists_file(target_dir.join("bin/yarn.cmd")).unwrap()); - - remove_dir_all_force(target_dir).await.unwrap(); - } - - #[tokio::test] - async fn test_get_latest_version() { - let result = get_latest_version(PackageManagerType::Yarn).await; - assert!(result.is_ok()); - let version = result.unwrap(); - // println!("version: {:?}", version); - assert!(!version.is_empty()); - // check version should >= 4.0.0 - let version_req = VersionReq::parse(">=4.0.0"); - assert!(version_req.is_ok()); - let version_req = version_req.unwrap(); - assert!(version_req.matches(&Version::parse(&version).unwrap())); - } - - #[tokio::test] - async fn test_detect_package_manager_with_yarnrc_yml() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create .yarnrc.yml - fs::write( - temp_dir_path.join(".yarnrc.yml"), - "nodeLinker: node-modules\nyarnPath: .yarn/releases/yarn-4.0.0.cjs", - ) - .expect("Failed to write .yarnrc.yml"); - - let result = PackageManager::builder(temp_dir_path.clone()) - .build() - .await - .expect("Should detect yarn from .yarnrc.yml"); - assert_eq!(result.bin_name, "yarn"); - assert_eq!(result.workspace_root, temp_dir_path); - assert!( - result.get_bin_prefix().ends_with("yarn/bin"), - "bin_prefix should end with yarn/bin, but got {:?}", - result.get_bin_prefix() - ); - // package.json should have the `packageManager` field - let package_json_path = temp_dir.path().join("package.json"); - let package_json: serde_json::Value = - serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap(); - assert!(package_json["packageManager"].as_str().unwrap().starts_with("yarn@")); - // keep other fields - assert_eq!(package_json["name"].as_str().unwrap(), "test-package"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_pnpmfile_cjs() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create pnpmfile.cjs - fs::write(temp_dir_path.join("pnpmfile.cjs"), "module.exports = { hooks: {} }") - .expect("Failed to write pnpmfile.cjs"); - - let result = PackageManager::builder(temp_dir_path.clone()) - .build() - .await - .expect("Should detect pnpm from pnpmfile.cjs"); - assert_eq!(result.bin_name, "pnpm"); - assert_eq!(result.workspace_root, temp_dir_path); - assert!( - result.get_bin_prefix().ends_with("pnpm/bin"), - "bin_prefix should end with pnpm/bin, but got {:?}", - result.get_bin_prefix() - ); - // package.json should have the `packageManager` field - let package_json_path = temp_dir_path.join("package.json"); - let package_json: serde_json::Value = - serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap(); - assert!(package_json["packageManager"].as_str().unwrap().starts_with("pnpm@")); - // keep other fields - assert_eq!(package_json["name"].as_str().unwrap(), "test-package"); - } - - #[tokio::test] - async fn test_detect_package_manager_with_yarn_config_cjs() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create yarn.config.cjs - fs::write( - temp_dir_path.join("yarn.config.cjs"), - "module.exports = { nodeLinker: 'node-modules' }", - ) - .expect("Failed to write yarn.config.cjs"); - - let result = PackageManager::builder(temp_dir_path.clone()) - .build() - .await - .expect("Should detect yarn from yarn.config.cjs"); - assert_eq!(result.bin_name, "yarn"); - assert_eq!(result.workspace_root, temp_dir_path); - assert!( - result.get_bin_prefix().ends_with("yarn/bin"), - "bin_prefix should end with yarn/bin, but got {:?}", - result.get_bin_prefix() - ); - // package.json should have the `packageManager` field - let package_json_path = temp_dir_path.join("package.json"); - let package_json: serde_json::Value = - serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap(); - assert!(package_json["packageManager"].as_str().unwrap().starts_with("yarn@")); - // keep other fields - assert_eq!(package_json["name"].as_str().unwrap(), "test-package"); - } - - #[tokio::test] - async fn test_detect_package_manager_priority_order_lock_over_config() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create multiple detection files to test priority order - // According to vite-install.md, pnpmfile.cjs and yarn.config.cjs are lower priority than lock files - - // Create pnpmfile.cjs - fs::write(temp_dir_path.join("pnpmfile.cjs"), "module.exports = { hooks: {} }") - .expect("Failed to write pnpmfile.cjs"); - - // Create yarn.config.cjs - fs::write( - temp_dir_path.join("yarn.config.cjs"), - "module.exports = { nodeLinker: 'node-modules' }", - ) - .expect("Failed to write yarn.config.cjs"); - - // Create package-lock.json (should take precedence over pnpmfile.cjs and yarn.config.cjs) - fs::write(temp_dir_path.join("package-lock.json"), r#"{"lockfileVersion": 3}"#) - .expect("Failed to write package-lock.json"); - - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect npm from package-lock.json"); - assert_eq!( - result.bin_name, "npm", - "package-lock.json should take precedence over pnpmfile.cjs and yarn.config.cjs" - ); - } - - #[tokio::test] - async fn test_detect_package_manager_pnpmfile_over_yarn_config() { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_content = r#"{"name": "test-package"}"#; - create_package_json(&temp_dir_path, package_content); - - // Create both pnpmfile.cjs and yarn.config.cjs - fs::write(temp_dir_path.join("pnpmfile.cjs"), "module.exports = { hooks: {} }") - .expect("Failed to write pnpmfile.cjs"); - - fs::write( - temp_dir_path.join("yarn.config.cjs"), - "module.exports = { nodeLinker: 'node-modules' }", - ) - .expect("Failed to write yarn.config.cjs"); - - // pnpmfile.cjs should be detected first (before yarn.config.cjs) - let result = PackageManager::builder(temp_dir_path) - .build() - .await - .expect("Should detect pnpm from pnpmfile.cjs"); - assert_eq!( - result.bin_name, "pnpm", - "pnpmfile.cjs should be detected before yarn.config.cjs" - ); - } - // Tests for get_fingerprint_ignores method - mod get_fingerprint_ignores_tests { - use vite_glob::GlobPatternSet; - - use super::*; - - fn create_mock_package_manager(pm_type: PackageManagerType) -> PackageManager { - let temp_dir = create_temp_dir(); - let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let install_dir = temp_dir_path.join("install"); - - PackageManager { - client: pm_type, - package_name: pm_type.to_string().into(), - version: "1.0.0".into(), - hash: None, - bin_name: pm_type.to_string().into(), - workspace_root: temp_dir_path, - install_dir, - } - } - - #[test] - fn test_pnpm_fingerprint_ignores() { - let pm = create_mock_package_manager(PackageManagerType::Pnpm); - let ignores = pm.get_fingerprint_ignores(); - let matcher = GlobPatternSet::new(&ignores).expect("Should compile patterns"); - - // Should ignore most files in node_modules - assert!( - matcher.is_match("node_modules/pkg-a/index.js"), - "Should ignore implementation files" - ); - assert!( - matcher.is_match("foo/bar/node_modules/pkg-a/lib/util.js"), - "Should ignore nested files" - ); - assert!(matcher.is_match("node_modules/.bin/cli"), "Should ignore binaries"); - - // Should NOT ignore package.json files (including in node_modules) - assert!(!matcher.is_match("package.json"), "Should NOT ignore root package.json"); - assert!( - !matcher.is_match("packages/app/package.json"), - "Should NOT ignore package package.json" - ); - - // Should ignore package.json files under node_modules - assert!( - matcher.is_match("node_modules/pkg-a/package.json"), - "Should ignore package.json in node_modules" - ); - assert!( - matcher.is_match("foo/bar/node_modules/pkg-a/package.json"), - "Should ignore package.json in node_modules" - ); - assert!( - matcher.is_match("node_modules/@scope/pkg-a/package.json"), - "Should ignore package.json in node_modules" - ); - - // Should keep node_modules directories themselves - assert!(!matcher.is_match("node_modules"), "Should NOT ignore node_modules directory"); - assert!( - !matcher.is_match("packages/app/node_modules"), - "Should NOT ignore nested node_modules" - ); - assert!( - matcher.is_match("node_modules/mqtt/node_modules"), - "Should ignore sub node_modules under node_modules" - ); - assert!( - matcher - .is_match("node_modules/minimatch/node_modules/brace-expansion/node_modules"), - "Should ignore sub node_modules under node_modules" - ); - assert!( - matcher.is_match("packages/app/node_modules/@octokit/graphql/node_modules"), - "Should ignore sub node_modules under node_modules" - ); - - // Should keep the root scoped directory under node_modules - assert!(!matcher.is_match("node_modules/@types"), "Should NOT ignore scoped directory"); - assert!( - matcher.is_match("node_modules/@types/node"), - "Should ignore scoped sub directory" - ); - - // Pnpm-specific files should NOT be ignored - assert!( - !matcher.is_match("pnpm-workspace.yaml"), - "Should NOT ignore pnpm-workspace.yaml" - ); - assert!(!matcher.is_match("pnpm-lock.yaml"), "Should NOT ignore pnpm-lock.yaml"); - assert!(!matcher.is_match(".pnpmfile.cjs"), "Should NOT ignore .pnpmfile.cjs"); - assert!(!matcher.is_match("pnpmfile.cjs"), "Should NOT ignore pnpmfile.cjs"); - assert!(!matcher.is_match(".pnp.cjs"), "Should NOT ignore .pnp.cjs"); - assert!(!matcher.is_match(".npmrc"), "Should NOT ignore .npmrc"); - - // Other package manager files should be ignored - assert!(matcher.is_match("yarn.lock"), "Should ignore yarn.lock"); - assert!(matcher.is_match("package-lock.json"), "Should ignore package-lock.json"); - - // Regular source files should be ignored - assert!(matcher.is_match("src/index.js"), "Should ignore source files"); - assert!(matcher.is_match("dist/bundle.js"), "Should ignore build outputs"); - } - - #[test] - fn test_yarn_fingerprint_ignores() { - let pm = create_mock_package_manager(PackageManagerType::Yarn); - let ignores = pm.get_fingerprint_ignores(); - let matcher = GlobPatternSet::new(&ignores).expect("Should compile patterns"); - - // Should ignore most files in node_modules - assert!( - matcher.is_match("node_modules/react/index.js"), - "Should ignore implementation files" - ); - assert!( - matcher.is_match("node_modules/react/cjs/react.production.js"), - "Should ignore nested files" - ); - - // Should NOT ignore package.json files (including in node_modules) - assert!(!matcher.is_match("package.json"), "Should NOT ignore root package.json"); - assert!( - !matcher.is_match("apps/web/package.json"), - "Should NOT ignore app package.json" - ); - - // Should ignore package.json files under node_modules - assert!( - matcher.is_match("node_modules/react/package.json"), - "Should ignore package.json in node_modules" - ); - - // Should keep node_modules directories - assert!(!matcher.is_match("node_modules"), "Should NOT ignore node_modules directory"); - assert!(!matcher.is_match("node_modules/@types"), "Should NOT ignore scoped packages"); - - // Yarn-specific files should NOT be ignored - assert!(!matcher.is_match(".yarnrc"), "Should NOT ignore .yarnrc"); - assert!(!matcher.is_match(".yarnrc.yml"), "Should NOT ignore .yarnrc.yml"); - assert!(!matcher.is_match("yarn.config.cjs"), "Should NOT ignore yarn.config.cjs"); - assert!(!matcher.is_match("yarn.lock"), "Should NOT ignore yarn.lock"); - assert!( - !matcher.is_match(".yarn/releases/yarn-4.0.0.cjs"), - "Should NOT ignore .yarn contents" - ); - assert!( - !matcher.is_match(".yarn/patches/package.patch"), - "Should NOT ignore .yarn patches" - ); - assert!( - !matcher.is_match(".yarn/patches/yjs-npm-13.6.21-c9f1f3397c.patch"), - "Should NOT ignore .yarn patches" - ); - assert!(!matcher.is_match(".pnp.cjs"), "Should NOT ignore .pnp.cjs"); - assert!(!matcher.is_match(".npmrc"), "Should NOT ignore .npmrc"); - - // Other package manager files should be ignored - assert!(matcher.is_match("pnpm-lock.yaml"), "Should ignore pnpm-lock.yaml"); - assert!(matcher.is_match("package-lock.json"), "Should ignore package-lock.json"); - - // Regular source files should be ignored - assert!(matcher.is_match("src/components/Button.tsx"), "Should ignore source files"); - - // Should ignore nested node_modules - assert!( - matcher.is_match( - "node_modules/@mixmark-io/domino/.yarn/plugins/@yarnpkg/plugin-version.cjs" - ), - "Should ignore sub node_modules under node_modules" - ); - assert!( - matcher.is_match("node_modules/touch/node_modules"), - "Should ignore sub node_modules under node_modules" - ); - } - - #[test] - fn test_npm_fingerprint_ignores() { - let pm = create_mock_package_manager(PackageManagerType::Npm); - let ignores = pm.get_fingerprint_ignores(); - let matcher = GlobPatternSet::new(&ignores).expect("Should compile patterns"); - - // Should ignore most files in node_modules - assert!( - matcher.is_match("node_modules/express/index.js"), - "Should ignore implementation files" - ); - assert!( - matcher.is_match("node_modules/express/lib/application.js"), - "Should ignore nested files" - ); - - // Should NOT ignore package.json files (including in node_modules) - assert!(!matcher.is_match("package.json"), "Should NOT ignore root package.json"); - assert!(!matcher.is_match("src/package.json"), "Should NOT ignore nested package.json"); - - // Should ignore package.json files under node_modules - assert!( - matcher.is_match("node_modules/express/package.json"), - "Should ignore package.json in node_modules" - ); - - // Should keep node_modules directories - assert!(!matcher.is_match("node_modules"), "Should NOT ignore node_modules directory"); - assert!(!matcher.is_match("node_modules/@babel"), "Should NOT ignore scoped packages"); - - // Npm-specific files should NOT be ignored - assert!(!matcher.is_match("package-lock.json"), "Should NOT ignore package-lock.json"); - assert!( - !matcher.is_match("npm-shrinkwrap.json"), - "Should NOT ignore npm-shrinkwrap.json" - ); - assert!(!matcher.is_match(".npmrc"), "Should NOT ignore .npmrc"); - - // Other package manager files should be ignored - assert!(matcher.is_match("pnpm-lock.yaml"), "Should ignore pnpm-lock.yaml"); - assert!(matcher.is_match("yarn.lock"), "Should ignore yarn.lock"); - - // Regular files should be ignored - assert!(matcher.is_match("README.md"), "Should ignore docs"); - assert!(matcher.is_match("src/app.ts"), "Should ignore source files"); - } - } -} diff --git a/crates/vite_install/src/request.rs b/crates/vite_install/src/request.rs deleted file mode 100644 index 650490b4..00000000 --- a/crates/vite_install/src/request.rs +++ /dev/null @@ -1,624 +0,0 @@ -use std::{path::Path, time::Duration}; - -use backon::{ExponentialBuilder, Retryable}; -use flate2::read::GzDecoder; -use futures_util::stream::StreamExt; -use reqwest::Response; -use serde::de::DeserializeOwned; -use sha1::Sha1; -use sha2::{Digest, Sha224, Sha256, Sha512}; -use tar::Archive; -use tokio::{fs, io::AsyncWriteExt}; -use vite_error::Error; - -/// HTTP client with built-in retry support -#[derive(Clone)] -pub struct HttpClient { - max_times: usize, - min_delay: u64, -} - -impl Default for HttpClient { - fn default() -> Self { - Self::new() - } -} - -impl HttpClient { - /// Create a new HTTP client with default settings (3 retries, 500ms min delay) - pub const fn new() -> Self { - Self::with_config(3, 500) - } - - /// Create a new HTTP client with custom retry configuration - /// - /// # Arguments - /// - /// * `max_times` - Maximum number of retry attempts - /// * `min_delay` - Minimum delay in milliseconds for exponential backoff - pub const fn with_config(max_times: usize, min_delay: u64) -> Self { - Self { max_times, min_delay } - } - - async fn get(&self, url: &str) -> Result { - let response = (|| async { reqwest::get(url).await?.error_for_status() }) - .retry( - ExponentialBuilder::default() - .with_jitter() - .with_min_delay(Duration::from_millis(self.min_delay)) - .with_max_times(self.max_times), - ) - .await?; - - Ok(response) - } - - /// Get JSON data from a URL - /// - /// # Arguments - /// - /// * `url` - The URL to fetch JSON from - /// - /// # Returns - /// - /// * `Ok(T)` - Deserialized JSON data - /// * `Err(e)` - If the request fails or JSON deserialization fails - pub async fn get_json(&self, url: &str) -> Result { - tracing::debug!("Fetching JSON from: {}", url); - - let response = self.get(url).await?; - let data = response.json::().await?; - Ok(data) - } - - /// Download a file to a specified path - /// - /// # Arguments - /// - /// * `url` - The URL of the file to download - /// * `target_path` - The path where the file will be saved - /// - /// # Returns - /// - /// * `Ok(())` - If the file is downloaded successfully - /// * `Err(e)` - If the download fails - pub async fn download_file( - &self, - url: &str, - target_path: impl AsRef, - ) -> Result<(), Error> { - let target_path = target_path.as_ref(); - tracing::debug!("Downloading {} to {:?}", url, target_path); - - let response = self.get(url).await?; - - self.write_response_to_file(response, target_path).await?; - - tracing::debug!("Download completed: {:?}", target_path); - Ok(()) - } - - /// Internal helper to write response body to file - async fn write_response_to_file( - &self, - response: Response, - target_path: &Path, - ) -> Result<(), Error> { - // Create the target file - let mut file = fs::File::create(target_path).await?; - - // Stream the response body to the file - let mut stream = response.bytes_stream(); - while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result?; - file.write_all(&chunk).await?; - } - - file.flush().await?; - Ok(()) - } -} - -fn extract_tgz(tgz_file: impl AsRef, target_dir: impl AsRef) -> Result<(), Error> { - let tgz_file = tgz_file.as_ref(); - let target_dir = target_dir.as_ref(); - tracing::debug!("Extract tgz: {:?} to {:?}", tgz_file, target_dir); - - let file = std::fs::File::open(tgz_file)?; - let tar_stream = GzDecoder::new(file); - let mut archive = Archive::new(tar_stream); - archive.unpack(target_dir)?; - - tracing::debug!("Extract tgz finished"); - - Ok(()) -} - -/// Download a tgz file from a URL and extract it to a target directory with optional hash verification. -/// -/// # Arguments -/// * `url` - The URL of the tgz file to download. -/// * `target_dir` - The directory to extract the tgz file to. -/// * `expected_hash` - Optional expected hash in format "algorithm.hash" (e.g., "sha512.abcd1234...") -/// -/// # Returns -/// * `Ok(())` - If the tgz file is downloaded, verified (if hash provided) and extracted successfully. -/// * `Err(e)` - If the tgz file is not downloaded, verified or extracted successfully. -pub async fn download_and_extract_tgz_with_hash( - url: &str, - target_dir: impl AsRef, - expected_hash: Option<&str>, -) -> Result<(), Error> { - let target_dir = target_dir.as_ref().to_path_buf(); - tracing::debug!( - "Start download and extract {} to {:?}, expected hash: {:?}", - url, - target_dir, - expected_hash - ); - - // Create target directory - fs::create_dir_all(&target_dir).await?; - - // Download the tgz file with retry logic using HttpClient - let tgz_file = target_dir.join("package.tgz"); - let client = HttpClient::new(); - client.download_file(url, &tgz_file).await?; - - // Verify hash if provided - if let Some(expected_hash) = expected_hash { - verify_file_hash(&tgz_file, expected_hash).await?; - } - - // Extract the tgz file to the target directory - let tgz_file_for_extract = tgz_file.clone(); - let target_dir_for_extract = target_dir.clone(); - tokio::task::spawn_blocking(move || { - extract_tgz(&tgz_file_for_extract, &target_dir_for_extract) - }) - .await??; - - // Remove the temp file - fs::remove_file(&tgz_file).await?; - tracing::debug!("Download and extract finished"); - Ok(()) -} - -/// Computes the hash of the given content using the specified digest algorithm. -/// -/// # Type Parameters -/// * `D` - A type that implements the [`Digest`] trait, such as `Sha256`, `Sha512`, etc. -/// -/// # Arguments -/// * `content` - The byte slice to hash. -/// -/// # Returns -/// A hex-encoded string representing the computed digest. -fn compute_hash(content: &[u8]) -> String { - let mut hasher = D::new(); - hasher.update(content); - hex::encode(hasher.finalize()) -} - -/// Verify the hash of a file against an expected hash. -/// -/// # Arguments -/// * `file_path` - Path to the file to verify -/// * `expected_hash` - Expected hash in format "algorithm.hash" (e.g., "sha512.abcd1234...") -/// -/// # Returns -/// * `Ok(())` - If the file hash matches the expected hash -/// * `Err(Error::HashMismatch)` - If the file hash doesn't match -pub async fn verify_file_hash( - file_path: impl AsRef, - expected_hash: &str, -) -> Result<(), Error> { - let file_path = file_path.as_ref(); - let content = fs::read(file_path).await?; - - // Parse the hash format (e.g., "sha512.abcd1234..." or "sha256.abcd1234...") - let (algorithm, expected_hex) = if let Some((algo, hash)) = expected_hash.split_once('.') { - (algo, hash) - } else { - return Err(Error::InvalidHashFormat(expected_hash.into())); - }; - - // Calculate the actual hash based on the algorithm - let actual_hex = match algorithm { - "sha512" => compute_hash::(&content), - "sha256" => compute_hash::(&content), - "sha224" => compute_hash::(&content), - "sha1" => compute_hash::(&content), - _ => return Err(Error::UnsupportedHashAlgorithm(algorithm.into())), - }; - - if actual_hex != expected_hex { - return Err(Error::HashMismatch { - expected: expected_hash.into(), - actual: format!("{algorithm}.{actual_hex}").into(), - }); - } - - tracing::debug!("Hash verification successful"); - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::fs; - - use httpmock::prelude::*; - use tempfile::TempDir; - - use super::*; - - /// Helper function to create a mock package tar.gz that mimics npm package structure - fn create_mock_package_tgz() -> Vec { - let mut tar_builder = tar::Builder::new(Vec::new()); - - // Add package.json - let package_json = br#"{"name":"test-package","version":"1.0.0"}"#; - let mut header = tar::Header::new_gnu(); - header.set_size(package_json.len() as u64); - header.set_mode(0o644); - tar_builder - .append_data(&mut header, "package/package.json", std::io::Cursor::new(package_json)) - .unwrap(); - - // Add bin/yarn mock file - let yarn_content = b"#!/usr/bin/env node\nconsole.log('mock yarn');"; - let mut header = tar::Header::new_gnu(); - header.set_size(yarn_content.len() as u64); - header.set_mode(0o755); - tar_builder - .append_data(&mut header, "package/bin/yarn", std::io::Cursor::new(yarn_content)) - .unwrap(); - - // Add bin/yarn.cmd mock file - let yarn_cmd_content = b"@echo off\nnode yarn %*"; - let mut header = tar::Header::new_gnu(); - header.set_size(yarn_cmd_content.len() as u64); - header.set_mode(0o755); - tar_builder - .append_data( - &mut header, - "package/bin/yarn.cmd", - std::io::Cursor::new(yarn_cmd_content), - ) - .unwrap(); - - let tar_data = tar_builder.into_inner().unwrap(); - - // Compress with gzip - let mut gz_data = Vec::new(); - { - let mut encoder = - flate2::write::GzEncoder::new(&mut gz_data, flate2::Compression::default()); - std::io::copy(&mut std::io::Cursor::new(tar_data), &mut encoder).unwrap(); - } - gz_data - } - - #[tokio::test] - #[test_log::test] - async fn test_extract_tgz_function() { - // Test the extract_tgz function directly - let temp_dir = TempDir::new().unwrap(); - let target_dir = temp_dir.path().join("extracted"); - - // Create a simple tar.gz file content for testing - let test_content = b"test file content"; - let mut tar_builder = tar::Builder::new(Vec::new()); - let mut header = tar::Header::new_gnu(); - header.set_size(test_content.len() as u64); - tar_builder - .append_data(&mut header, "test.txt", std::io::Cursor::new(test_content)) - .unwrap(); - let tar_data = tar_builder.into_inner().unwrap(); - - // Compress with gzip - let mut gz_data = Vec::new(); - { - let mut encoder = - flate2::write::GzEncoder::new(&mut gz_data, flate2::Compression::default()); - std::io::copy(&mut std::io::Cursor::new(tar_data), &mut encoder).unwrap(); - } - - // Write the compressed data to a temporary file - let tgz_file = temp_dir.path().join("test.tgz"); - fs::write(&tgz_file, gz_data).unwrap(); - - // Test extraction - let result = extract_tgz(&tgz_file, &target_dir); - assert!(result.is_ok()); - - // Verify the file was extracted - let extracted_file = target_dir.join("test.txt"); - assert!(extracted_file.exists()); - - // Verify the content - let content = fs::read_to_string(extracted_file).unwrap(); - assert_eq!(content, "test file content"); - } - - #[tokio::test] - async fn test_extract_tgz_large_file() { - // Test extraction with a larger file - let temp_dir = TempDir::new().unwrap(); - let target_dir = temp_dir.path().join("extracted"); - - // Create a larger tar.gz file for testing - let large_content = vec![b'a'; 1024 * 1024]; // 1MB - let mut tar_builder = tar::Builder::new(Vec::new()); - let mut header = tar::Header::new_gnu(); - header.set_size(large_content.len() as u64); - tar_builder - .append_data(&mut header, "large.txt", std::io::Cursor::new(&large_content)) - .unwrap(); - let tar_data = tar_builder.into_inner().unwrap(); - - // Compress with gzip - let mut gz_data = Vec::new(); - { - let mut encoder = - flate2::write::GzEncoder::new(&mut gz_data, flate2::Compression::default()); - std::io::copy(&mut std::io::Cursor::new(tar_data), &mut encoder).unwrap(); - } - - // Write the compressed data to a temporary file - let tgz_file = temp_dir.path().join("large.tgz"); - fs::write(&tgz_file, gz_data).unwrap(); - - // Test extraction - let result = extract_tgz(&tgz_file, &target_dir); - assert!(result.is_ok()); - - // Verify the file was extracted - let extracted_file = target_dir.join("large.txt"); - assert!(extracted_file.exists()); - - // Verify the content size - let content = fs::read(extracted_file).unwrap(); - assert_eq!(content.len(), 1024 * 1024); - } - - #[tokio::test] - async fn test_extract_tgz_invalid_file() { - // Test extraction with invalid tar.gz content - let temp_dir = TempDir::new().unwrap(); - let target_dir = temp_dir.path().join("extracted"); - - // Create an invalid tar.gz file - let invalid_content = b"this is not a valid tar.gz file"; - let tgz_file = temp_dir.path().join("invalid.tgz"); - fs::write(&tgz_file, invalid_content).unwrap(); - - // Test extraction - should fail gracefully - let result = extract_tgz(&tgz_file, &target_dir); - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_extract_tgz_empty_file() { - // Test extraction with empty tar.gz - let temp_dir = TempDir::new().unwrap(); - let target_dir = temp_dir.path().join("extracted"); - - // Create an empty tar.gz file - let tgz_file = temp_dir.path().join("empty.tgz"); - fs::write(&tgz_file, Vec::::new()).unwrap(); - - // Test extraction - should handle empty file gracefully - let result = extract_tgz(&tgz_file, &target_dir); - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_http_client_get_json() { - #[derive(serde::Deserialize, Debug, PartialEq)] - struct PackageInfo { - name: String, - version: String, - description: String, - } - - let server = MockServer::start(); - - // Create mock JSON response - let mock_json = serde_json::json!({ - "name": "test-package", - "version": "1.0.0", - "description": "A test package" - }); - - server.mock(|when, then| { - when.method(GET).path("/api/package.json"); - then.status(200) - .header("content-type", "application/json") - .json_body(mock_json.clone()); - }); - - let client = HttpClient::new(); - let url = format!("{}/api/package.json", server.base_url()); - - let result: Result = client.get_json(&url).await; - assert!(result.is_ok()); - - let package_info = result.unwrap(); - assert_eq!(package_info.name, "test-package"); - assert_eq!(package_info.version, "1.0.0"); - assert_eq!(package_info.description, "A test package"); - } - - #[tokio::test] - async fn test_http_client_download_file() { - let server = MockServer::start(); - let temp_dir = TempDir::new().unwrap(); - let target_file = temp_dir.path().join("downloaded.txt"); - - let mock_content = b"Hello, World! This is test content."; - - server.mock(|when, then| { - when.method(GET).path("/file.txt"); - then.status(200).header("content-type", "text/plain").body(mock_content); - }); - - let client = HttpClient::new(); - let url = format!("{}/file.txt", server.base_url()); - - let result = client.download_file(&url, &target_file).await; - assert!(result.is_ok(), "Failed to download file: {result:?}"); - - // Verify file exists and has correct content - assert!(target_file.exists()); - let content = fs::read(&target_file).unwrap(); - assert_eq!(content, mock_content); - } - - #[tokio::test] - async fn test_http_client_retry_on_server_error() { - // Test that the client correctly retries on server errors - let server = MockServer::start(); - let temp_dir = TempDir::new().unwrap(); - let target_file = temp_dir.path().join("test.txt"); - - server.mock(|when, then| { - when.method(GET).path("/server_error"); - then.status(500).body("Internal Server Error"); - }); - - let client = HttpClient::with_config(2, 50); // 2 retries with 50ms base interval - let url = format!("{}/server_error", server.base_url()); - - // Should fail after retries - let result = client.download_file(&url, &target_file).await; - // println!("result: {:?}", result); - assert!(result.is_err(), "Expected download to fail with 500 after retries"); - } - - #[tokio::test] - async fn test_download_and_extract_tgz() { - // Start a mock server - let server = MockServer::start(); - let temp_dir = TempDir::new().unwrap(); - let target_dir = temp_dir.path().join("extracted"); - - // Create mock response with package tar.gz - let mock_tgz = create_mock_package_tgz(); - server.mock(|when, then| { - when.method(GET).path("/test-package.tgz"); - then.status(200).header("content-type", "application/octet-stream").body(mock_tgz); - }); - - let url = format!("{}/test-package.tgz", server.base_url()); - let result = download_and_extract_tgz_with_hash(&url, &target_dir, None).await; - assert!(result.is_ok(), "Failed to download and extract: {result:?}"); - - assert!(target_dir.join("package/bin/yarn").exists()); - assert!(target_dir.join("package/bin/yarn.cmd").exists()); - - // TempDir automatically cleans up when it goes out of scope - } - - #[tokio::test] - async fn test_verify_file_hash_sha1() { - use sha1::Sha1; - use sha2::Digest; - use tokio::io::AsyncWriteExt; - - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - - // Write test content - let content = b"Hello, World!"; - let mut file = tokio::fs::File::create(&test_file).await.unwrap(); - file.write_all(content).await.unwrap(); - - // Calculate expected SHA1 - let mut hasher = Sha1::new(); - hasher.update(content); - let expected_hash = format!("sha1.{:x}", hasher.finalize()); - - // Test successful verification - let result = verify_file_hash(&test_file, &expected_hash).await; - assert!(result.is_ok()); - - // Test failed verification - let wrong_hash = "sha1.0000000000000000000000000000000000000000"; - let result = verify_file_hash(&test_file, wrong_hash).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_verify_file_hash_sha224() { - use sha2::{Digest, Sha224}; - use tokio::io::AsyncWriteExt; - - let temp_dir = TempDir::new().unwrap(); - let test_file = temp_dir.path().join("test.txt"); - - // Write test content - let content = b"Test content for SHA224"; - let mut file = tokio::fs::File::create(&test_file).await.unwrap(); - file.write_all(content).await.unwrap(); - - // Calculate expected SHA224 - let mut hasher = Sha224::new(); - hasher.update(content); - let expected_hash = format!("sha224.{:x}", hasher.finalize()); - - // Test successful verification - let result = verify_file_hash(&test_file, &expected_hash).await; - assert!(result.is_ok()); - - // Test failed verification - let wrong_hash = "sha224.00000000000000000000000000000000000000000000000000000000"; - let result = verify_file_hash(&test_file, wrong_hash).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_http_client_download_with_404_error() { - let server = MockServer::start(); - let temp_dir = TempDir::new().unwrap(); - let target_file = temp_dir.path().join("test.txt"); - - // Mock a 404 response - let mock = server.mock(|when, then| { - when.method(GET).path("/nonexistent"); - then.status(404).body("Not Found"); - }); - - let client = HttpClient::new(); - let url = format!("{}/nonexistent", server.base_url()); - - // Should fail with 404 - let result = client.download_file(&url, &target_file).await; - assert!(result.is_err(), "Expected download to fail with 404"); - - // Should try 4 times, 1 for first request, 3 for retries - mock.assert_hits(4); - } - - #[tokio::test] - async fn test_http_client_json_with_invalid_response() { - #[derive(serde::Deserialize)] - struct TestData { - _field: String, - } - - let server = MockServer::start(); - - // Mock response with invalid JSON - server.mock(|when, then| { - when.method(GET).path("/invalid.json"); - then.status(200).header("content-type", "application/json").body("not valid json"); - }); - - let client = HttpClient::new(); - let url = format!("{}/invalid.json", server.base_url()); - - let result: Result = client.get_json(&url).await; - assert!(result.is_err(), "Expected JSON parsing to fail"); - } -} diff --git a/crates/vite_install/src/shim.rs b/crates/vite_install/src/shim.rs deleted file mode 100644 index d238b346..00000000 --- a/crates/vite_install/src/shim.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::path::Path; - -use indoc::formatdoc; -use pathdiff::diff_paths; -use tokio::fs::write; -use vite_error::Error; - -/// Write cmd/sh/pwsh shim files. -pub async fn write_shims( - source_file: impl AsRef, - to_bin: impl AsRef, -) -> Result<(), Error> { - let to_bin = to_bin.as_ref(); - // source file `/foo/bar/pnpm.js` point to bin file `/foo/bin/npm`, the relative path is `../bar/pnpm.js`. - let relative_path = diff_paths(source_file, to_bin.parent().unwrap()).unwrap(); - let relative_file = relative_path.to_str().unwrap(); - - // Referenced from pnpm/cmd-shim's TypeScript implementation: - // https://github.com/pnpm/cmd-shim/blob/main/src/index.ts - write(to_bin, sh_shim(relative_file)).await?; - write(to_bin.with_extension("cmd"), cmd_shim(relative_file)).await?; - write(to_bin.with_extension("ps1"), pwsh_shim(relative_file)).await?; - - // set executable permission for unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - tokio::fs::set_permissions(to_bin, std::fs::Permissions::from_mode(0o755)).await?; - } - Ok(()) -} - -/// Unix shell shim. -pub fn sh_shim(relative_file: &str) -> String { - formatdoc! { - r#" - #!/bin/sh - basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") - - case `uname` in - *CYGWIN*|*MINGW*|*MSYS*) - if command -v cygpath > /dev/null 2>&1; then - basedir=`cygpath -w "$basedir"` - fi - ;; - esac - - if [ -x "$basedir/node" ]; then - exec "$basedir/node" "$basedir/{relative_file}" "$@" - else - exec node "$basedir/{relative_file}" "$@" - fi - "# - } -} - -/// Windows Command Prompt shim. -pub fn cmd_shim(relative_file: &str) -> String { - formatdoc! { - r#" - @SETLOCAL - @IF EXIST "%~dp0\node.exe" ( - "%~dp0\node.exe" "%~dp0\{relative_file}" %* - ) ELSE ( - @SET PATHEXT=%PATHEXT:;.JS;=;% - node "%~dp0\{relative_file}" %* - ) - "#, - relative_file = relative_file.replace('/', "\\") - } - .replace('\n', "\r\n") // replace \n to \r\n for windows -} - -/// `PowerShell` shim. -pub fn pwsh_shim(relative_file: &str) -> String { - formatdoc! { - r#" - #!/usr/bin/env pwsh - $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent - - $exe="" - if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {{ - # Fix case when both the Windows and Linux builds of Node - # are installed in the same directory - $exe=".exe" - }} - $ret=0 - if (Test-Path "$basedir/node$exe") {{ - # Support pipeline input - if ($MyInvocation.ExpectingInput) {{ - $input | & "$basedir/node$exe" "$basedir/{relative_file}" $args - }} else {{ - & "$basedir/node$exe" "$basedir/{relative_file}" $args - }} - $ret=$LASTEXITCODE - }} else {{ - # Support pipeline input - if ($MyInvocation.ExpectingInput) {{ - $input | & "node$exe" "$basedir/{relative_file}" $args - }} else {{ - & "node$exe" "$basedir/{relative_file}" $args - }} - $ret=$LASTEXITCODE - }} - exit $ret - "# - } -} - -#[cfg(test)] -#[cfg(not(windows))] // FIXME -mod tests { - use tempfile::TempDir; - use tokio::fs::read_to_string; - - use super::*; - - fn format_shim(shim: &str) -> String { - shim.replace(' ', "·") - } - - #[test] - fn test_sh_shim() { - let shim = sh_shim("pnpm.js"); - // println!("{:#}", format_shim(&shim)); - assert!(shim.contains("pnpm.js"), "{}", format_shim(&shim)); - } - - #[test] - fn test_cmd_shim() { - let shim = cmd_shim("yarn.js"); - // println!("{:#?}", format_shim(&shim)); - assert!(shim.contains("yarn.js"), "{}", format_shim(&shim)); - assert!( - shim.contains("@SETLOCAL\r\n@IF EXIST \"%~dp0\\node.exe\" (\r\n"), - "{}", - format_shim(&shim) - ); - - let shim = cmd_shim("../../../../pnpm.js"); - // println!("{:#}", format_shim(&shim)); - assert!( - shim.contains("node \"%~dp0\\..\\..\\..\\..\\pnpm.js\" %*"), - "{}", - format_shim(&shim) - ); - assert!( - shim.contains("@SETLOCAL\r\n@IF EXIST \"%~dp0\\node.exe\" (\r\n"), - "{}", - format_shim(&shim) - ); - } - - #[test] - fn test_pwsh_shim() { - let shim = pwsh_shim("pnpm.cjs"); - // println!("{:#}", format_shim(&shim)); - assert!(shim.contains("pnpm.cjs"), "{}", format_shim(&shim)); - } - - #[tokio::test] - async fn test_write_shims_basic() { - let temp_dir = TempDir::new().unwrap(); - let source = temp_dir.path().join("node_modules").join(".bin").join("pnpm.js"); - let target = temp_dir.path().join("bin").join("pnpm"); - - // Create parent directories - tokio::fs::create_dir_all(source.parent().unwrap()).await.unwrap(); - tokio::fs::create_dir_all(target.parent().unwrap()).await.unwrap(); - - // Write shims - write_shims(&source, &target).await.unwrap(); - - // Verify base shim file was created (shell script) - assert!(target.exists()); - let content = read_to_string(&target).await.unwrap(); - assert!(content.contains("#!/bin/sh")); - assert!(content.contains("../node_modules/.bin/pnpm.js")); - - // Verify .cmd file was created - let cmd_file = target.with_extension("cmd"); - assert!(cmd_file.exists()); - let cmd_content = read_to_string(&cmd_file).await.unwrap(); - assert!(cmd_content.contains("@SETLOCAL")); - assert!(cmd_content.contains("..\\node_modules\\.bin\\pnpm.js")); - - // Verify .ps1 file was created - let ps1_file = target.with_extension("ps1"); - assert!(ps1_file.exists()); - let ps1_content = read_to_string(&ps1_file).await.unwrap(); - assert!(ps1_content.contains("#!/usr/bin/env pwsh")); - assert!(ps1_content.contains("../node_modules/.bin/pnpm.js")); - } - - #[tokio::test] - async fn test_write_shims_relative_paths() { - let temp_dir = TempDir::new().unwrap(); - - // Test case 1: Source is deeper than target - let source1 = temp_dir.path().join("deep").join("nested").join("path").join("script.js"); - let target1 = temp_dir.path().join("bin").join("script"); - - tokio::fs::create_dir_all(source1.parent().unwrap()).await.unwrap(); - tokio::fs::create_dir_all(target1.parent().unwrap()).await.unwrap(); - - write_shims(&source1, &target1).await.unwrap(); - - let content1 = read_to_string(&target1).await.unwrap(); - assert!(content1.contains("../deep/nested/path/script.js")); - - // Test case 2: Source and target at same level - let source2 = temp_dir.path().join("scripts").join("tool.js"); - let target2 = temp_dir.path().join("bin").join("tool"); - - tokio::fs::create_dir_all(source2.parent().unwrap()).await.unwrap(); - tokio::fs::create_dir_all(target2.parent().unwrap()).await.unwrap(); - - write_shims(&source2, &target2).await.unwrap(); - - let content2 = read_to_string(&target2).await.unwrap(); - assert!(content2.contains("../scripts/tool.js")); - } - - #[tokio::test] - async fn test_write_shims_windows_path_conversion() { - let temp_dir = TempDir::new().unwrap(); - let source = temp_dir.path().join("node_modules").join("package").join("bin.js"); - let target = temp_dir.path().join("bin").join("package"); - - tokio::fs::create_dir_all(source.parent().unwrap()).await.unwrap(); - tokio::fs::create_dir_all(target.parent().unwrap()).await.unwrap(); - - write_shims(&source, &target).await.unwrap(); - - // Check base file (shell script) has forward slashes - let content = read_to_string(&target).await.unwrap(); - assert!(content.contains("../node_modules/package/bin.js")); - - // Check CMD file has backslashes - let cmd_file = target.with_extension("cmd"); - let cmd_content = read_to_string(&cmd_file).await.unwrap(); - assert!(cmd_content.contains("..\\node_modules\\package\\bin.js")); - assert!(!cmd_content.contains("../node_modules/package/bin.js")); - - // Check PS1 file has forward slashes - let ps1_file = target.with_extension("ps1"); - let ps1_content = read_to_string(&ps1_file).await.unwrap(); - assert!(ps1_content.contains("../node_modules/package/bin.js")); - assert!(!ps1_content.contains("..\\node_modules\\")); - } - - #[tokio::test] - async fn test_write_shims_overwrite_existing() { - let temp_dir = TempDir::new().unwrap(); - let source = temp_dir.path().join("src").join("cli.js"); - let target = temp_dir.path().join("bin").join("cli"); - - tokio::fs::create_dir_all(source.parent().unwrap()).await.unwrap(); - tokio::fs::create_dir_all(target.parent().unwrap()).await.unwrap(); - - // Write initial content to files - tokio::fs::write(&target, "old content").await.unwrap(); - tokio::fs::write(target.with_extension("cmd"), "old cmd content").await.unwrap(); - tokio::fs::write(target.with_extension("ps1"), "old ps1 content").await.unwrap(); - - // Write shims (should overwrite) - write_shims(&source, &target).await.unwrap(); - - // Verify files were overwritten - let content = read_to_string(&target).await.unwrap(); - assert!(!content.contains("old content")); - assert!(content.contains("../src/cli.js")); - - let cmd_content = read_to_string(target.with_extension("cmd")).await.unwrap(); - assert!(!cmd_content.contains("old cmd content")); - assert!(cmd_content.contains("@SETLOCAL")); - - let ps1_content = read_to_string(target.with_extension("ps1")).await.unwrap(); - assert!(!ps1_content.contains("old ps1 content")); - assert!(ps1_content.contains("#!/usr/bin/env pwsh")); - } - - #[tokio::test] - async fn test_write_shims_complex_relative_path() { - let temp_dir = TempDir::new().unwrap(); - let source = temp_dir.path().join("a").join("b").join("c").join("script.js"); - let target = temp_dir.path().join("x").join("y").join("z").join("script"); - - tokio::fs::create_dir_all(source.parent().unwrap()).await.unwrap(); - tokio::fs::create_dir_all(target.parent().unwrap()).await.unwrap(); - - write_shims(&source, &target).await.unwrap(); - - // Base file should be shell script with forward slashes - let content = read_to_string(&target).await.unwrap(); - assert!(content.contains("#!/bin/sh")); - assert!(content.contains("../../a/b/c/script.js")); - - // CMD file should have backslashes - let cmd_content = read_to_string(target.with_extension("cmd")).await.unwrap(); - assert!(cmd_content.contains("..\\..\\a\\b\\c\\script.js")); - } - - #[tokio::test] - async fn test_sh_shim_content_validation() { - let shim = sh_shim("lib/cli.js"); - - // Verify shebang - assert!(shim.starts_with("#!/bin/sh")); - - // Verify CYGWIN/MINGW/MSYS handling - assert!(shim.contains("*CYGWIN*|*MINGW*|*MSYS*)")); - assert!(shim.contains("cygpath -w")); - - // Verify node execution paths - assert!(shim.contains("if [ -x \"$basedir/node\" ]")); - assert!(shim.contains("exec \"$basedir/node\" \"$basedir/lib/cli.js\" \"$@\"")); - assert!(shim.contains("exec node \"$basedir/lib/cli.js\" \"$@\"")); - } - - #[tokio::test] - async fn test_cmd_shim_content_validation() { - let shim = cmd_shim("lib/cli.js"); - - // Verify Windows batch commands - assert!(shim.starts_with("@SETLOCAL")); - assert!(shim.contains("@IF EXIST \"%~dp0\\node.exe\"")); - assert!(shim.contains("\"%~dp0\\node.exe\" \"%~dp0\\lib\\cli.js\" %*")); - assert!(shim.contains("@SET PATHEXT=%PATHEXT:;.JS;=;%")); - assert!(shim.contains("node \"%~dp0\\lib\\cli.js\" %*")); - - // Verify line endings are Windows-style - assert!(shim.contains("\r\n")); - assert!(!shim.contains("\n\n")); // No double Unix line endings - } - - #[tokio::test] - async fn test_pwsh_shim_content_validation() { - let shim = pwsh_shim("lib/cli.js"); - - // Verify shebang - assert!(shim.starts_with("#!/usr/bin/env pwsh")); - - // Verify PowerShell version handling - assert!(shim.contains("$PSVersionTable.PSVersion -lt \"6.0\"")); - assert!(shim.contains("$IsWindows")); - - // Verify execution paths - assert!(shim.contains("Test-Path \"$basedir/node$exe\"")); - assert!(shim.contains("& \"$basedir/node$exe\" \"$basedir/lib/cli.js\" $args")); - - // Verify pipeline input support - assert!(shim.contains("$MyInvocation.ExpectingInput")); - assert!(shim.contains("$input |")); - - // Verify exit code handling - assert!(shim.contains("$ret=$LASTEXITCODE")); - assert!(shim.contains("exit $ret")); - } - - #[tokio::test] - async fn test_write_shims_error_handling() { - // Test with invalid path (no parent directory) - let temp_dir = TempDir::new().unwrap(); - let source = temp_dir.path().join("source.js"); - let target = temp_dir.path().join("non").join("existent").join("path").join("target"); - - // This should fail because parent directory doesn't exist - let result = write_shims(&source, &target).await; - assert!(result.is_err()); - } - - #[test] - fn test_cmd_shim_path_separator_conversion() { - // Test forward slashes are converted to backslashes - let shim = cmd_shim("node_modules/.bin/tool.js"); - assert!(shim.contains("node_modules\\.bin\\tool.js")); - assert!(!shim.contains("node_modules/.bin/tool.js")); - - // Test multiple levels - let shim = cmd_shim("a/b/c/d.js"); - assert!(shim.contains("a\\b\\c\\d.js")); - assert!(!shim.contains("a/b/c/d.js")); - } - - #[test] - fn test_relative_path_formats() { - // Test various relative path formats work correctly - let paths = vec![ - "../script.js", - "../../lib/cli.js", - "../../../node_modules/.bin/tool.js", - "script.js", - "./script.js", - ]; - - for path in paths { - let sh = sh_shim(path); - assert!(sh.contains(path)); - - let ps1 = pwsh_shim(path); - assert!(ps1.contains(path)); - - let cmd = cmd_shim(path); - let expected_cmd_path = path.replace('/', "\\"); - assert!(cmd.contains(&expected_cmd_path)); - } - } -} diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 4f2fc4ac..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.vitepress/cache diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts deleted file mode 100644 index 00c02b93..00000000 --- a/docs/.vitepress/config.mts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'vitepress'; - -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: 'Vite+', - description: 'Vite+', - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: 'Home', link: '/' }, - ], - - sidebar: [], - - socialLinks: [], - }, -}); diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 764d619e..00000000 --- a/docs/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "Vite+" - text: "Vite+" - tagline: My great project tagline - actions: - - theme: brand - text: AAA - link: / - - theme: alt - text: BBB - link: / - -features: - - title: Feature A - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature B - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature C - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit ---- diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 512147f7..00000000 --- a/docs/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "vite-docs", - "type": "module", - "private": true, - "scripts": { - "dev": "vitepress dev", - "build": "vitepress build", - "preview": "vitepress preview" - }, - "devDependencies": { - "vue": "catalog:", - "vitepress": "catalog:", - "oxc-minify": "catalog:" - } -} diff --git a/dprint.json b/dprint.json index 4d016d20..16a591a5 100644 --- a/dprint.json +++ b/dprint.json @@ -14,9 +14,7 @@ }, "excludes": [ "crates/fspy_detours_sys/detours", - "pnpm-lock.yaml", - "packages/cli/binding/index.d.ts", - "packages/cli/binding/index.js" + "pnpm-lock.yaml" ], "plugins": [ "https://plugins.dprint.dev/typescript-0.94.0.wasm", diff --git a/mise.toml b/mise.toml deleted file mode 100644 index 362c01a2..00000000 --- a/mise.toml +++ /dev/null @@ -1,5 +0,0 @@ -[settings] -idiomatic_version_file_enable_tools = [] - -[tools] -node = "22.18.0" diff --git a/package.json b/package.json index 917c81bf..26bba7c7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "vite-plus-monorepo", + "name": "vite-task-monorepo", "license": "BUSL-1.1", "private": true, "packageManager": "pnpm@10.17.1", @@ -8,27 +8,11 @@ }, "type": "module", "scripts": { - "bootstrap-cli": "pnpm --filter=@voidzero-dev/vite-plus build && pnpm --filter=@voidzero-dev/global build && pnpm copy-bindings && pnpm install-global-cli", - "bootstrap-cli:ci": "pnpm --filter=@voidzero-dev/global build && pnpm install-global-cli", - "copy-bindings": "cp -r ./packages/cli/binding/*.node ./packages/cli/dist && cp -r ./packages/cli/binding/*.node ./packages/global/dist", - "install-global-cli": "npm install -g ./packages/global", - "typecheck": "tsc -b tsconfig.json", - "lint": "vite lint && vite run typecheck", - "test": "vite test run && pnpm -r snap-test", "prepare": "husky" }, "devDependencies": { - "@oxc-node/cli": "catalog:", - "@oxc-node/core": "catalog:", - "@types/node": "catalog:", - "@voidzero-dev/vite-plus": "workspace:*", - "@voidzero-dev/vite-plus-tools": "workspace:*", "husky": "catalog:", - "lint-staged": "catalog:", - "oxfmt": "catalog:", - "oxlint": "catalog:", - "typescript": "catalog:", - "vitest": "catalog:" + "lint-staged": "catalog:" }, "lint-staged": { "*.@(js|ts|tsx|yml|yaml|md|json|html|toml)": [ diff --git a/packages/cli/README.md b/packages/cli/README.md deleted file mode 100644 index 7c323c34..00000000 --- a/packages/cli/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Vite+ Local CLI Package - -## Overview - -This package provides the JavaScript-to-Rust bridge that enables vite-plus to execute JavaScript tooling (like Vite, Vitest, and oxlint) from the Rust core. It uses NAPI-RS to create native Node.js bindings. - -## Usage - -### Install - -Add to your project's devDependencies: - -```bash -pnpm add -D @voidzero-dev/vite-plus -# or -npm install -D @voidzero-dev/vite-plus -# or -yarn add -D @voidzero-dev/vite-plus -``` - -### Built-in Commands - -#### Build - -build command will use `rolldown-vite` to build your project. - -```bash -npx vite build -``` - -#### Test - -test command will use `vitest` to test your project. - -```bash -npx vite test -``` - -#### Lint - -lint command will use `oxlint` to lint your project. - -```bash -npx vite lint -``` - -#### Task runner - -You can use `vite run` to run any task that you want. - -Run a task on the current project. - -```bash -npx vite run -``` - -Run all task with the same name in monorepo. - -```bash -npx vite run -r -``` - -## Architecture - -### How It Works - -The architecture follows a callback-based pattern where JavaScript functions resolve tool paths and pass them to Rust for execution: - -``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ JavaScript │────▶│ NAPI Bridge │────▶│ Rust Core │ -│ (bin.ts) │ │ (binding/) │ │ (vite_task) │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - │ │ │ - ▼ ▼ ▼ - Resolves tool Converts JS Executes tools - binary paths callbacks to Rust with resolved paths -``` - -### Key Components - -#### 1. JavaScript Layer (`src/`) - -The JavaScript layer is responsible for resolving tool binary paths: - -- **`bin.ts`**: Entry point that initializes the CLI with tool resolvers -- **`vite.ts`**: Resolves the Vite binary path for build commands -- **`test.ts`**: Resolves the Vitest binary path for test commands -- **`lint.ts`**: Resolves the oxlint binary path for linting -- **`index.ts`**: Exports the `defineConfig` helper for Vite configuration - -Each resolver function returns: - -```typescript -{ - binPath: string, // Absolute path to the tool's binary - envs: Record // Environment variables to set -} -``` - -#### 2. NAPI Binding Layer (`binding/`) - -The binding layer provides the JavaScript-to-Rust bridge using NAPI-RS: - -- **`src/lib.rs`**: Defines the NAPI bindings and type conversions -- **`index.d.ts`**: TypeScript type definitions (auto-generated) -- **`index.js`**: Native module loader (auto-generated) - -The binding converts JavaScript callbacks into Rust futures using `ThreadsafeFunction`. - -#### 3. Rust Core Integration - -The Rust core (`crates/vite_task`) receives the tool resolvers through `CliOptions`: - -```rust -pub struct CliOptions { - pub lint: LintFn, // Callback to resolve lint tool - pub vite: ViteFn, // Callback to resolve vite tool - pub test: TestFn, // Callback to resolve test tool -} -``` - -## Execution Flow - -1. **Initialization**: `bin.ts` calls `run()` with tool resolver functions -2. **Command Parsing**: Rust parses CLI arguments to determine which command to run -3. **Tool Resolution**: When a command needs a tool (e.g., `vite build`): - - Rust calls back to JavaScript through NAPI - - JavaScript resolver finds the tool's binary path - - Path is returned to Rust -4. **Execution**: Rust executes the tool binary with appropriate arguments - -## Example: Vite Build Command - -When a user runs `vite-plus build`: - -1. Rust identifies this as a Build command -2. Calls the `vite` callback function -3. JavaScript `vite.ts` resolves `vite/bin/vite.js` path -4. Returns path to Rust -5. Rust executes: `node /path/to/vite.js build [args]` - -## Development - -### Building - -```bash -# Build the native binding -pnpm build - -# Or watch for changes -pnpm build:debug -``` - -### Adding a New Tool - -1. Create a resolver in `src/`: - -```typescript -// src/mytool.ts -export async function mytool() { - const binPath = require.resolve('mytool/bin/cli.js'); - return { binPath, envs: {} }; -} -``` - -2. Add to `CliOptions` in `binding/src/lib.rs`: - -```rust -pub struct CliOptions { - // ... existing fields - pub mytool: Arc>>, -} -``` - -3. Wire it up in `bin.ts`: - -```typescript -import { mytool } from './mytool.js'; -run({ lint, vite, test, mytool }); -``` - -## Benefits of This Architecture - -1. **Tool Resolution in JavaScript**: Leverages Node.js module resolution to find tools -2. **Execution in Rust**: Benefits from Rust's performance and concurrency -3. **Type Safety**: Full type safety across the JS-Rust boundary -4. **Flexibility**: Easy to add new tools without changing core logic -5. **Environment Handling**: Can pass environment variables per tool - -## Dependencies - -- `napi`: Node-API bindings for Rust -- `napi-derive`: Procedural macros for NAPI -- `vite`, `vitest`, `oxlint`: The actual tools being wrapped diff --git a/packages/cli/bin/vite b/packages/cli/bin/vite deleted file mode 100755 index ed32f675..00000000 --- a/packages/cli/bin/vite +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -import module from 'node:module' -if (module.enableCompileCache) { - module.enableCompileCache() -} -import '../dist/bin.js' diff --git a/packages/cli/binding/.gitignore b/packages/cli/binding/.gitignore deleted file mode 100644 index 0292f649..00000000 --- a/packages/cli/binding/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.node -*.wasm \ No newline at end of file diff --git a/packages/cli/binding/Cargo.toml b/packages/cli/binding/Cargo.toml deleted file mode 100644 index 6ea22e2d..00000000 --- a/packages/cli/binding/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "vite-plus-cli" -version = "0.0.0" -edition = "2024" - -[[bin]] -name = "vite" -path = "src/main.rs" - -[dependencies] -clap = { workspace = true, features = ["derive"] } -crossterm = { workspace = true } -napi = { workspace = true } -napi-derive = { workspace = true } -petgraph = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["fs"] } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -vite_error = { workspace = true } -vite_install = { workspace = true } -vite_path = { workspace = true } -vite_str = { workspace = true } -vite_task = { workspace = true } - -[build-dependencies] -napi-build = { workspace = true } - -[dev-dependencies] -serial_test = { workspace = true } -tempfile = { workspace = true } - -[lib] -crate-type = ["cdylib"] diff --git a/packages/cli/binding/build.rs b/packages/cli/binding/build.rs deleted file mode 100644 index 0f1b0100..00000000 --- a/packages/cli/binding/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - napi_build::setup(); -} diff --git a/packages/cli/binding/index.d.ts b/packages/cli/binding/index.d.ts deleted file mode 100644 index 9a94ee6b..00000000 --- a/packages/cli/binding/index.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* auto-generated by NAPI-RS */ -/* eslint-disable */ -/** - * Configuration options passed from JavaScript to Rust. - * - * Each field (except `cwd`) is a JavaScript function wrapped in a `ThreadsafeFunction`. - * These functions are called by Rust to resolve tool binary paths when needed. - * - * The `ThreadsafeFunction` wrapper ensures the JavaScript functions can be - * safely called from Rust's async runtime without blocking or race conditions. - */ -export interface CliOptions { - /** Resolver function for the lint tool (oxlint) */ - lint: ((err: Error | null, ) => Promise) - /** Resolver function for the fmt tool (oxfmt) */ - fmt: ((err: Error | null, ) => Promise) - /** Resolver function for the vite tool (used for build/dev) */ - vite: ((err: Error | null, ) => Promise) - /** Resolver function for the test tool (vitest) */ - test: ((err: Error | null, ) => Promise) - /** Resolver function for the lib tool (tsdown) */ - lib: ((err: Error | null, ) => Promise) - /** Resolver function for the doc tool (vitepress) */ - doc: ((err: Error | null, ) => Promise) - /** Optional working directory override */ - cwd?: string - /** Read the vite.config.ts in the Node.js side and return the `lint` and `fmt` config JSON string back to the Rust side */ - resolveUniversalViteConfig: ((err: Error | null, arg: string) => Promise) -} - -/** - * Result returned by JavaScript resolver functions. - * - * This structure contains the information needed to execute a tool: - * - `bin_path`: The absolute path to the tool's binary/script - * - `envs`: Environment variables to set when executing the tool - */ -export interface JsCommandResolvedResult { - /** Absolute path to the tool's executable or script */ - binPath: string - /** Environment variables to set when running the tool */ - envs: Record -} - -/** - * Main entry point for the CLI, called from JavaScript. - * - * This function: - * 1. Parses command-line arguments - * 2. Sets up the working directory - * 3. Creates Rust-callable wrappers for JavaScript resolver functions - * 4. Passes control to the Rust core (`vite_task::main`) - * - * ## JavaScript-to-Rust Bridge - * - * The resolver functions are wrapped to: - * - Call the JavaScript function asynchronously - * - Handle errors and convert them to Rust error types - * - Convert the JavaScript result to Rust's expected format - * - * ## Error Handling - * - * Errors from JavaScript resolvers are converted to specific error types - * (e.g., `LintFailed`, `ViteError`) to provide better error messages. - */ -export declare function run(options: CliOptions): Promise diff --git a/packages/cli/binding/index.js b/packages/cli/binding/index.js deleted file mode 100644 index b73c0c9a..00000000 --- a/packages/cli/binding/index.js +++ /dev/null @@ -1,579 +0,0 @@ -// prettier-ignore -/* eslint-disable */ -// @ts-nocheck -/* auto-generated by NAPI-RS */ - -import { createRequire } from 'node:module' -const require = createRequire(import.meta.url) -const __dirname = new URL('.', import.meta.url).pathname - -const { readFileSync } = require('node:fs') -let nativeBinding = null -const loadErrors = [] - -const isMusl = () => { - let musl = false - if (process.platform === 'linux') { - musl = isMuslFromFilesystem() - if (musl === null) { - musl = isMuslFromReport() - } - if (musl === null) { - musl = isMuslFromChildProcess() - } - } - return musl -} - -const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') - -const isMuslFromFilesystem = () => { - try { - return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') - } catch { - return null - } -} - -const isMuslFromReport = () => { - let report = null - if (typeof process.report?.getReport === 'function') { - process.report.excludeNetwork = true - report = process.report.getReport() - } - if (!report) { - return null - } - if (report.header && report.header.glibcVersionRuntime) { - return false - } - if (Array.isArray(report.sharedObjects)) { - if (report.sharedObjects.some(isFileMusl)) { - return true - } - } - return false -} - -const isMuslFromChildProcess = () => { - try { - return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') - } catch (e) { - // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false - return false - } -} - -function requireNative() { - if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { - try { - return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); - } catch (err) { - loadErrors.push(err) - } - } else if (process.platform === 'android') { - if (process.arch === 'arm64') { - try { - return require('./vite-plus.android-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-android-arm64') - const bindingPackageVersion = require('@vite-plus-android-arm64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm') { - try { - return require('./vite-plus.android-arm-eabi.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-android-arm-eabi') - const bindingPackageVersion = require('@vite-plus-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) - } - } else if (process.platform === 'win32') { - if (process.arch === 'x64') { - if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') { - try { - return require('./vite-plus.win32-x64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-win32-x64-gnu') - const bindingPackageVersion = require('@vite-plus-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./vite-plus.win32-x64-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-win32-x64-msvc') - const bindingPackageVersion = require('@vite-plus-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'ia32') { - try { - return require('./vite-plus.win32-ia32-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-win32-ia32-msvc') - const bindingPackageVersion = require('@vite-plus-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./vite-plus.win32-arm64-msvc.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-win32-arm64-msvc') - const bindingPackageVersion = require('@vite-plus-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) - } - } else if (process.platform === 'darwin') { - try { - return require('./vite-plus.darwin-universal.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-darwin-universal') - const bindingPackageVersion = require('@vite-plus-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - if (process.arch === 'x64') { - try { - return require('./vite-plus.darwin-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-darwin-x64') - const bindingPackageVersion = require('@vite-plus-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./vite-plus.darwin-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-darwin-arm64') - const bindingPackageVersion = require('@vite-plus-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) - } - } else if (process.platform === 'freebsd') { - if (process.arch === 'x64') { - try { - return require('./vite-plus.freebsd-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-freebsd-x64') - const bindingPackageVersion = require('@vite-plus-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm64') { - try { - return require('./vite-plus.freebsd-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-freebsd-arm64') - const bindingPackageVersion = require('@vite-plus-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) - } - } else if (process.platform === 'linux') { - if (process.arch === 'x64') { - if (isMusl()) { - try { - return require('./vite-plus.linux-x64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-x64-musl') - const bindingPackageVersion = require('@vite-plus-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./vite-plus.linux-x64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-x64-gnu') - const bindingPackageVersion = require('@vite-plus-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'arm64') { - if (isMusl()) { - try { - return require('./vite-plus.linux-arm64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-arm64-musl') - const bindingPackageVersion = require('@vite-plus-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./vite-plus.linux-arm64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-arm64-gnu') - const bindingPackageVersion = require('@vite-plus-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'arm') { - if (isMusl()) { - try { - return require('./vite-plus.linux-arm-musleabihf.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-arm-musleabihf') - const bindingPackageVersion = require('@vite-plus-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./vite-plus.linux-arm-gnueabihf.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-arm-gnueabihf') - const bindingPackageVersion = require('@vite-plus-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'loong64') { - if (isMusl()) { - try { - return require('./vite-plus.linux-loong64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-loong64-musl') - const bindingPackageVersion = require('@vite-plus-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./vite-plus.linux-loong64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-loong64-gnu') - const bindingPackageVersion = require('@vite-plus-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'riscv64') { - if (isMusl()) { - try { - return require('./vite-plus.linux-riscv64-musl.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-riscv64-musl') - const bindingPackageVersion = require('@vite-plus-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - try { - return require('./vite-plus.linux-riscv64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-riscv64-gnu') - const bindingPackageVersion = require('@vite-plus-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } - } else if (process.arch === 'ppc64') { - try { - return require('./vite-plus.linux-ppc64-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-ppc64-gnu') - const bindingPackageVersion = require('@vite-plus-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 's390x') { - try { - return require('./vite-plus.linux-s390x-gnu.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-linux-s390x-gnu') - const bindingPackageVersion = require('@vite-plus-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) - } - } else if (process.platform === 'openharmony') { - if (process.arch === 'arm64') { - try { - return require('./vite-plus.openharmony-arm64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-openharmony-arm64') - const bindingPackageVersion = require('@vite-plus-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'x64') { - try { - return require('./vite-plus.openharmony-x64.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-openharmony-x64') - const bindingPackageVersion = require('@vite-plus-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else if (process.arch === 'arm') { - try { - return require('./vite-plus.openharmony-arm.node') - } catch (e) { - loadErrors.push(e) - } - try { - const binding = require('@vite-plus-openharmony-arm') - const bindingPackageVersion = require('@vite-plus-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) - } - return binding - } catch (e) { - loadErrors.push(e) - } - } else { - loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) - } - } else { - loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) - } -} - -nativeBinding = requireNative() - -if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { - let wasiBinding = null - let wasiBindingError = null - try { - wasiBinding = require('./vite-plus.wasi.cjs') - nativeBinding = wasiBinding - } catch (err) { - if (process.env.NAPI_RS_FORCE_WASI) { - wasiBindingError = err - } - } - if (!nativeBinding) { - try { - wasiBinding = require('@vite-plus-wasm32-wasi') - nativeBinding = wasiBinding - } catch (err) { - if (process.env.NAPI_RS_FORCE_WASI) { - wasiBindingError.cause = err - loadErrors.push(err) - } - } - } - if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { - const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') - error.cause = wasiBindingError - throw error - } -} - -if (!nativeBinding) { - if (loadErrors.length > 0) { - throw new Error( - `Cannot find native binding. ` + - `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + - 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', - { - cause: loadErrors.reduce((err, cur) => { - cur.cause = err - return cur - }), - }, - ) - } - throw new Error(`Failed to load native binding`) -} - -const { run } = nativeBinding -export { run } diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs deleted file mode 100644 index a53b01a6..00000000 --- a/packages/cli/binding/src/cli.rs +++ /dev/null @@ -1,2632 +0,0 @@ -//! CLI types and logic moved from vite_task -//! -//! This module contains all the CLI-related code. -//! It handles argument parsing, command dispatching, and orchestration of the task execution. - -use std::{future::Future, pin::Pin, process::ExitStatus, sync::Arc}; - -use clap::{Parser, Subcommand}; -use serde::{Deserialize, Serialize}; -use tokio::fs::write; -use vite_error::Error; -use vite_install::commands::{add::SaveDependencyType, outdated::Format}; -use vite_path::AbsolutePathBuf; -use vite_str::Str; -use vite_task::{ - CURRENT_EXECUTION_ID, EXECUTION_SUMMARY_DIR, ExecutionPlan, ExecutionStatus, ExecutionSummary, - ResolveCommandResult, TaskCache, Workspace, -}; - -use crate::commands::{ - add::AddCommand, - dedupe::DedupeCommand, - doc::doc as doc_cmd, - fmt::{FmtConfig, fmt}, - install::InstallCommand, - lib_cmd::lib, - link::LinkCommand, - lint::{LintConfig, lint}, - outdated::OutdatedCommand, - remove::RemoveCommand, - test::test, - unlink::UnlinkCommand, - update::UpdateCommand, - vite::vite as vite_cmd, - why::WhyCommand, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResolvedUniversalViteConfig { - pub lint: Option, - pub fmt: Option, -} - -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -pub struct Args { - pub task: Option, - - /// Optional arguments for the tasks, captured after '--'. - #[clap(last = true)] - pub task_args: Vec, - - #[clap(subcommand)] - pub commands: Commands, - - /// Display cache for debugging. - #[clap(short, long)] - pub debug: bool, - #[clap(long, conflicts_with = "debug")] - pub no_debug: bool, -} - -#[derive(Subcommand, Debug)] -pub enum Commands { - Run { - tasks: Vec, - #[clap(last = true)] - /// Optional arguments for the tasks, captured after '--'. - task_args: Vec, - #[clap(short, long)] - recursive: bool, - #[clap(long, conflicts_with = "recursive")] - no_recursive: bool, - #[clap(short, long)] - sequential: bool, - #[clap(long, conflicts_with = "sequential")] - no_sequential: bool, - #[clap(short, long)] - parallel: bool, - #[clap(long, conflicts_with = "parallel")] - no_parallel: bool, - #[clap(short, long)] - topological: Option, - #[clap(long, conflicts_with = "topological")] - no_topological: bool, - }, - Lint { - #[clap(last = true)] - /// Arguments to pass to oxlint - args: Vec, - }, - Fmt { - #[clap(last = true)] - /// Arguments to pass to oxfmt - args: Vec, - }, - Build { - #[clap(last = true)] - /// Arguments to pass to vite build - args: Vec, - }, - Test { - #[clap(last = true)] - /// Arguments to pass to vite test - args: Vec, - }, - /// Lib command, build a library - #[command(disable_help_flag = true)] - Lib { - /// Arguments to pass to tsdown - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - Dev { - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - /// Arguments to pass to vite dev - args: Vec, - }, - /// Doc command, build documentation - Doc { - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - /// Arguments to pass to vitepress - args: Vec, - }, - /// Manage the task cache - Cache { - #[clap(subcommand)] - subcmd: CacheSubcommand, - }, - // package manager commands - /// Install command. - /// It will be passed to the package manager's install command currently. - #[command(disable_help_flag = true, alias = "i")] - Install { - /// Arguments to pass to vite install - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - /// Add packages to dependencies - Add { - /// Save to `dependencies` (default) - #[arg(short = 'P', long)] - save_prod: bool, - /// Save to `devDependencies` - #[arg(short = 'D', long)] - save_dev: bool, - /// Save to `peerDependencies` and `devDependencies` - #[arg(long)] - save_peer: bool, - /// Save to `optionalDependencies` - #[arg(short = 'O', long)] - save_optional: bool, - /// Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) - #[arg(short = 'E', long)] - save_exact: bool, - - /// Save the new dependency to the specified catalog name. - /// Example: `vite add vue --save-catalog-name vue3` - #[arg(long, value_name = "CATALOG_NAME")] - save_catalog_name: Option, - /// Save the new dependency to the default catalog - #[arg(long)] - save_catalog: bool, - - /// A list of package names allowed to run postinstall - #[arg(long, value_name = "NAMES")] - allow_build: Option, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Add to workspace root (ignore-workspace-root-check) - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Only add if package exists in workspace (pnpm-specific) - #[arg(long)] - workspace: bool, - - /// Install globally - #[arg(short = 'g', long)] - global: bool, - - /// Packages to add - #[arg(required = true)] - packages: Vec, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - /// Remove packages from dependencies - #[command(alias = "rm", alias = "un", alias = "uninstall")] - Remove { - /// Only remove from `devDependencies` (pnpm-specific) - #[arg(short = 'D', long)] - save_dev: bool, - - /// Only remove from `optionalDependencies` (pnpm-specific) - #[arg(short = 'O', long)] - save_optional: bool, - - /// Only remove from `dependencies` (pnpm-specific) - #[arg(short = 'P', long)] - save_prod: bool, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Remove from workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Remove recursively from all workspace packages, including workspace root - #[arg(short = 'r', long)] - recursive: bool, - - /// Remove global packages - #[arg(short = 'g', long)] - global: bool, - - /// Packages to remove - #[arg(required = true)] - packages: Vec, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - /// Update packages to their latest versions - #[command(alias = "up")] - Update { - /// Update to latest version (ignore semver range) - #[arg(short = 'L', long)] - latest: bool, - - /// Update global packages - #[arg(short = 'g', long)] - global: bool, - - /// Update recursively in all workspace packages - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Include workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Update only devDependencies - #[arg(short = 'D', long)] - dev: bool, - - /// Update only dependencies (production) - #[arg(short = 'P', long)] - prod: bool, - - /// Interactive mode - show outdated packages and choose which to update - #[arg(short = 'i', long)] - interactive: bool, - - /// Don't update optionalDependencies - #[arg(long)] - no_optional: bool, - - /// Update lockfile only, don't modify package.json - #[arg(long)] - no_save: bool, - - /// Only update if package exists in workspace (pnpm-specific) - #[arg(long)] - workspace: bool, - - /// Packages to update (optional - updates all if omitted) - packages: Vec, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - /// Deduplicate dependencies by removing older versions - #[command(alias = "ddp")] - Dedupe { - /// Check if deduplication would make changes - #[arg(long)] - check: bool, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - /// Check for outdated packages - Outdated { - /// Package name(s) to check (supports glob patterns in pnpm) - packages: Vec, - - /// Show extended information - #[arg(long)] - long: bool, - - /// Output format: table (default), list, or json - #[arg(long, value_name = "FORMAT", value_parser = clap::value_parser!(Format))] - format: Option, - - /// Check recursively across all workspaces - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Include workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Only production and optional dependencies (pnpm-specific) - #[arg(short = 'P', long)] - prod: bool, - - /// Only dev dependencies (pnpm-specific) - #[arg(short = 'D', long)] - dev: bool, - - /// Exclude optional dependencies (pnpm-specific) - #[arg(long)] - no_optional: bool, - - /// Only show compatible versions (pnpm-specific) - #[arg(long)] - compatible: bool, - - /// Sort results by field (pnpm-specific) - #[arg(long, value_name = "FIELD")] - sort_by: Option, - - /// Check globally installed packages - #[arg(short = 'g', long)] - global: bool, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - /// Show why a package is installed - #[command(alias = "explain")] - Why { - /// Package(s) to check - #[arg(required = true)] - packages: Vec, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Show extended information (pnpm-specific) - #[arg(long)] - long: bool, - - /// Show parseable output (pnpm-specific) - #[arg(long)] - parseable: bool, - - /// Check recursively across all workspaces - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo (pnpm/npm-specific) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Check in workspace root (pnpm-specific) - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Only production dependencies (pnpm-specific) - #[arg(short = 'P', long)] - prod: bool, - - /// Only dev dependencies (pnpm-specific) - #[arg(short = 'D', long)] - dev: bool, - - /// Limit tree depth (pnpm-specific) - #[arg(long)] - depth: Option, - - /// Exclude optional dependencies (pnpm-specific) - #[arg(long)] - no_optional: bool, - - /// Check globally installed packages - #[arg(short = 'g', long)] - global: bool, - - /// Exclude peer dependencies (pnpm/yarn@2+-specific) - #[arg(long)] - exclude_peers: bool, - - /// Use a finder function defined in .pnpmfile.cjs (pnpm-specific) - #[arg(long, value_name = "FINDER_NAME")] - find_by: Option, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - /// Link packages for local development - #[command(alias = "ln")] - Link { - /// Package name or directory to link - /// If empty, registers current package globally - #[arg(value_name = "PACKAGE|DIR")] - package: Option, - - /// Arguments to pass to package manager - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - /// Unlink packages - Unlink { - /// Package name to unlink - /// If empty, unlinks current package globally - #[arg(value_name = "PACKAGE|DIR")] - package: Option, - - /// Unlink in every workspace package (pnpm/yarn@2+-specific) - #[arg(short = 'r', long)] - recursive: bool, - - /// Arguments to pass to package manager - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, -} - -impl Commands { - /// Check if this command is a package manager command that should skip auto-install - pub fn is_package_manager_command(&self) -> bool { - matches!( - self, - Commands::Install { .. } - | Commands::Add { .. } - | Commands::Remove { .. } - | Commands::Dedupe { .. } - | Commands::Outdated { .. } - | Commands::Why { .. } - | Commands::Link { .. } - | Commands::Unlink { .. } - ) - } -} - -#[derive(Subcommand, Debug)] -pub enum CacheSubcommand { - /// Clean up all the cache - Clean, - /// View the cache entries in json for debugging purpose - View, -} - -/// Resolve boolean flag value considering both positive and negative forms. -/// If the negative form (--no-*) is present, it takes precedence and returns false. -/// Otherwise, returns the value of the positive form. -const fn resolve_bool_flag(positive: bool, negative: bool) -> bool { - if negative { false } else { positive } -} - -/// Automatically run install command -async fn auto_install(workspace_root: &AbsolutePathBuf) -> Result<(), Error> { - // Skip if we're already running inside a vite_task execution to prevent nested installs - if std::env::var("VITE_TASK_EXECUTION_ENV").is_ok_and(|v| v == "1") { - tracing::debug!("Skipping auto-install: already running inside vite_task execution"); - return Ok(()); - } - - tracing::debug!("Running install automatically..."); - let _exit_status = InstallCommand::builder(workspace_root.clone()) - .ignore_replay() - .build() - .execute(&vec![]) - .await?; - // For auto-install, we don't propagate exit failures to avoid breaking the main command - Ok(()) -} - -pub struct CliOptions< - Lint: Future> = Pin< - Box>>, - >, - LintFn: Fn() -> Lint = Box Lint>, - Fmt: Future> = Pin< - Box>>, - >, - FmtFn: Fn() -> Fmt = Box Fmt>, - Vite: Future> = Pin< - Box>>, - >, - ViteFn: Fn() -> Vite = Box Vite>, - Test: Future> = Pin< - Box>>, - >, - TestFn: Fn() -> Test = Box Test>, - Lib: Future> = Pin< - Box>>, - >, - LibFn: Fn() -> Lib = Box Lib>, - Doc: Future> = Pin< - Box>>, - >, - DocFn: Fn() -> Doc = Box Doc>, - ResolveUniversalViteConfig: Future> = Pin< - Box>>, - >, - ResolveUniversalViteConfigFn: Fn(String) -> ResolveUniversalViteConfig = Box< - dyn Fn(String) -> ResolveUniversalViteConfig, - >, -> { - pub lint: LintFn, - pub fmt: FmtFn, - pub vite: ViteFn, - pub test: TestFn, - pub lib: LibFn, - pub doc: DocFn, - pub resolve_universal_vite_config: ResolveUniversalViteConfigFn, -} - -/// Main entry point for vite-plus task execution. -/// -/// # Execution Flow -/// -/// ```text -/// vite-plus run build --recursive --topological -/// │ -/// ▼ -/// 1. Load workspace -/// - Scan for packages and their dependencies -/// - Build complete task graph with all tasks and dependencies -/// - Parse compound commands (&&) into subtasks -/// - Add cross-package dependencies (same-name tasks) -/// - Resolve transitive dependencies (A→B→C even if B lacks task) -/// │ -/// ▼ -/// 2. Resolve tasks (filter pre-built graph) -/// - With --recursive: find all packages with requested task -/// - Without --recursive: use specific package task -/// - Extract subgraph including all dependencies -/// │ -/// ▼ -/// 3. Create execution plan -/// - Sort tasks by dependencies (topological sort) -/// │ -/// ▼ -/// 4. Execute plan -/// - For each task: check cache → execute/replay → update cache -/// ``` -#[tracing::instrument(skip(options))] -pub async fn main< - Lint: Future>, - LintFn: Fn() -> Lint, - Fmt: Future>, - FmtFn: Fn() -> Fmt, - Vite: Future>, - ViteFn: Fn() -> Vite, - Test: Future>, - TestFn: Fn() -> Test, - Lib: Future>, - LibFn: Fn() -> Lib, - Doc: Future>, - DocFn: Fn() -> Doc, - ResolveUniversalViteConfig: Future>, - ResolveUniversalViteConfigFn: Fn(String) -> ResolveUniversalViteConfig, ->( - cwd: AbsolutePathBuf, - mut args: Args, - options: Option< - CliOptions< - Lint, - LintFn, - Fmt, - FmtFn, - Vite, - ViteFn, - Test, - TestFn, - Lib, - LibFn, - Doc, - DocFn, - ResolveUniversalViteConfig, - ResolveUniversalViteConfigFn, - >, - >, -) -> Result { - // Auto-install dependencies if needed, but skip for package manager commands, or if `VITE_DISABLE_AUTO_INSTALL=1` is set. - if !args.commands.is_package_manager_command() - && std::env::var_os("VITE_DISABLE_AUTO_INSTALL") != Some("1".into()) - { - auto_install(&cwd).await?; - } - - let mut summary: ExecutionSummary = match &mut args.commands { - Commands::Run { - tasks, - recursive, - no_recursive, - parallel, - no_parallel, - topological, - no_topological, - task_args, - .. - } => { - let recursive_run = resolve_bool_flag(*recursive, *no_recursive); - let parallel_run = resolve_bool_flag(*parallel, *no_parallel); - // Note: topological dependencies are always included in the pre-built task graph - // This flag now mainly affects execution order in the execution plan - let topological_run = if *no_topological { - false - } else if let Some(t) = topological { - *t - } else { - recursive_run - }; - let workspace = Workspace::load(cwd, topological_run)?; - - let task_graph = workspace.build_task_subgraph( - tasks, - Arc::<[Str]>::from(task_args.clone()), - recursive_run, - )?; - - let plan = ExecutionPlan::plan(task_graph, parallel_run)?; - let summary = plan.execute(&workspace).await?; - workspace.unload().await?; - summary - } - Commands::Lint { args } => { - let workspace = Workspace::partial_load(cwd)?; - let lint_fn = options - .as_ref() - .map(|o| &o.lint) - .expect("lint command requires CliOptions to be provided"); - - let vite_config = read_vite_config_from_workspace_root( - workspace.root_dir(), - options.as_ref().map(|o| &o.resolve_universal_vite_config), - ) - .await?; - let resolved_vite_config: Option = vite_config - .map(|vite_config| { - serde_json::from_str(&vite_config).inspect_err(|_| { - tracing::error!("Failed to parse vite config: {vite_config}"); - }) - }) - .transpose()?; - let lint_config = resolved_vite_config.and_then(|c| c.lint); - if let Some(lint_config) = lint_config { - let oxlint_config_path = workspace.cache_path().join(".oxlintrc.json"); - write(&oxlint_config_path, serde_json::to_string(&lint_config)?).await?; - args.extend_from_slice(&[ - "--config".to_string(), - oxlint_config_path.as_path().to_string_lossy().into_owned(), - ]); - } - let summary = lint(lint_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Fmt { args } => { - let workspace = Workspace::partial_load(cwd)?; - let fmt_fn = - options.map(|o| o.fmt).expect("fmt command requires CliOptions to be provided"); - - let summary = fmt(fmt_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Build { args } => { - let workspace = Workspace::partial_load(cwd)?; - let vite_fn = - options.map(|o| o.vite).expect("build command requires CliOptions to be provided"); - - let summary = vite_cmd("build", vite_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Test { args } => { - let workspace = Workspace::partial_load(cwd)?; - let test_fn = - options.map(|o| o.test).expect("test command requires CliOptions to be provided"); - let summary = test(test_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Lib { args } => { - let workspace = Workspace::partial_load(cwd)?; - let lib_fn = - options.map(|o| o.lib).expect("lib command requires CliOptions to be provided"); - let summary = lib(lib_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Dev { args } => { - let workspace = Workspace::partial_load(cwd)?; - let vite_fn = options.map(|o| o.vite).expect("dev command requires CliOptions"); - let summary = vite_cmd("dev", vite_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Doc { args } => { - let workspace = Workspace::partial_load(cwd)?; - let doc_fn = options.map(|o| o.doc).expect("doc command requires CliOptions"); - let summary = doc_cmd(doc_fn, &workspace, args).await?; - workspace.unload().await?; - summary - } - Commands::Cache { subcmd } => { - let cache_path = Workspace::get_cache_path(&cwd)?; - match subcmd { - CacheSubcommand::Clean => { - std::fs::remove_dir_all(&cache_path)?; - } - CacheSubcommand::View => { - let cache = TaskCache::load_from_path(cache_path)?; - cache.list(std::io::stdout()).await?; - } - } - return Ok(ExitStatus::default()); - } - - // package manager commands - Commands::Install { args } => { - // Check if args contain packages - if yes, redirect to Add command - // This allows `vite install ` to work as an alias for `vite add ` - if let Some(Commands::Add { - filter, - workspace_root, - workspace, - packages, - save_prod, - save_dev, - save_peer, - save_optional, - save_exact, - save_catalog, - save_catalog_name, - global, - allow_build, - pass_through_args, - }) = parse_install_as_add(args) - { - let exit_status = execute_add_command( - cwd, - &packages, - save_prod, - save_dev, - save_peer, - save_optional, - save_exact, - save_catalog, - save_catalog_name.as_deref(), - filter.as_deref(), - workspace_root, - workspace, - global, - allow_build.as_deref(), - pass_through_args.as_deref(), - ) - .await?; - return Ok(exit_status); - } else { - InstallCommand::builder(cwd).build().execute(args).await? - } - } - Commands::Add { - filter, - workspace_root, - workspace, - packages, - save_prod, - save_dev, - save_peer, - save_optional, - save_exact, - save_catalog, - save_catalog_name, - global, - allow_build, - pass_through_args, - } => { - let exit_status = execute_add_command( - cwd, - packages, - *save_prod, - *save_dev, - *save_peer, - *save_optional, - *save_exact, - *save_catalog, - save_catalog_name.as_deref(), - filter.as_deref(), - *workspace_root, - *workspace, - *global, - allow_build.as_deref(), - pass_through_args.as_deref(), - ) - .await?; - return Ok(exit_status); - } - Commands::Remove { - save_dev, - save_optional, - save_prod, - filter, - workspace_root, - recursive, - global, - packages, - pass_through_args, - } => { - let exit_status = RemoveCommand::new(cwd) - .execute( - packages, - *save_dev, - *save_optional, - *save_prod, - filter.as_deref(), - *workspace_root, - *recursive, - *global, - pass_through_args.as_deref(), - ) - .await?; - return Ok(exit_status); - } - Commands::Update { - latest, - global, - recursive, - filter, - workspace_root, - dev, - prod, - interactive, - no_optional, - no_save, - workspace, - packages, - pass_through_args, - } => { - let exit_status = UpdateCommand::new(cwd) - .execute( - packages, - *latest, - *global, - *recursive, - filter.as_deref(), - *workspace_root, - *dev, - *prod, - *interactive, - *no_optional, - *no_save, - *workspace, - pass_through_args.as_deref(), - ) - .await?; - return Ok(exit_status); - } - Commands::Dedupe { check, pass_through_args } => { - let exit_status = - DedupeCommand::new(cwd).execute(*check, pass_through_args.as_deref()).await?; - return Ok(exit_status); - } - Commands::Outdated { - packages, - long, - format, - recursive, - filter, - workspace_root, - prod, - dev, - no_optional, - compatible, - sort_by, - global, - pass_through_args, - } => { - let exit_status = OutdatedCommand::new(cwd) - .execute( - packages, - *long, - *format, - *recursive, - filter.as_deref(), - *workspace_root, - *prod, - *dev, - *no_optional, - *compatible, - sort_by.as_deref(), - *global, - pass_through_args.as_deref(), - ) - .await?; - return Ok(exit_status); - } - Commands::Link { package, args } => { - let exit_status = LinkCommand::new(cwd).execute(package.as_deref(), Some(args)).await?; - return Ok(exit_status); - } - Commands::Unlink { package, recursive, args } => { - let exit_status = - UnlinkCommand::new(cwd).execute(package.as_deref(), *recursive, Some(args)).await?; - return Ok(exit_status); - } - Commands::Why { - packages, - json, - long, - parseable, - recursive, - filter, - workspace_root, - prod, - dev, - depth, - no_optional, - global, - exclude_peers, - find_by, - pass_through_args, - } => { - let exit_status = WhyCommand::new(cwd) - .execute( - packages, - *json, - *long, - *parseable, - *recursive, - filter.as_deref(), - *workspace_root, - *prod, - *dev, - *depth, - *no_optional, - *global, - *exclude_peers, - find_by.as_deref(), - pass_through_args.as_deref(), - ) - .await?; - return Ok(exit_status); - } - }; - - let execution_summary_dir = EXECUTION_SUMMARY_DIR.as_path(); - if let Some(current_execution_id) = &*CURRENT_EXECUTION_ID { - // We are in the inner runner, writing summary to EXECUTION_SUMMARY_DIR - let summary_path = execution_summary_dir.join(current_execution_id); - let summary_json = serde_json::to_string_pretty(&summary)?; - std::fs::write(summary_path, summary_json)?; - } else { - // We are in the outer runner, restoring summaries from EXECUTION_SUMMARY_DIR - loop { - // keep trying to restore until no more summaries can be restored - let mut next_restored_statuses: Vec = vec![]; - let mut has_newly_restored = false; - for status in &summary.execution_statuses { - let summary_path = execution_summary_dir.join(&status.execution_id); - let Ok(summary_json) = std::fs::read_to_string(summary_path) else { - next_restored_statuses.push(status.clone()); - continue; - }; - has_newly_restored = true; - let inner_summary: ExecutionSummary = serde_json::from_str(&summary_json).unwrap(); - next_restored_statuses.extend(inner_summary.execution_statuses); - } - summary.execution_statuses = next_restored_statuses; - if !has_newly_restored { - break; - } - } - - let _ = std::fs::remove_dir_all(execution_summary_dir); - if matches!(&args.commands, Commands::Run { .. }) { - print!("{}", &summary); - } - } - - // Return the first non-zero exit status, or zero if all succeeded - Ok(summary - .execution_statuses - .iter() - .find_map(|status| { - #[cfg(unix)] - use std::os::unix::process::ExitStatusExt; - #[cfg(windows)] - use std::os::windows::process::ExitStatusExt; - - // Err(ExecutionFailure) can be skipped because currently the only variant of `ExecutionFailure` is - // `SkippedDueToFailedDependency`, which means there must be at least one task with non-zero exit status. - if let Ok(exit_status) = status.execution_result - && let exit_status = ExitStatus::from_raw(exit_status as _) - && !exit_status.success() - { - Some(exit_status) - } else { - None - } - }) - .unwrap_or_default()) -} - -pub fn init_tracing() { - use std::sync::OnceLock; - - use tracing_subscriber::{ - filter::{LevelFilter, Targets}, - prelude::__tracing_subscriber_SubscriberExt, - util::SubscriberInitExt, - }; - - static TRACING: OnceLock<()> = OnceLock::new(); - TRACING.get_or_init(|| { - // Usage without the `regex` feature. - // - tracing_subscriber::registry() - .with( - std::env::var("VITE_LOG") - .map_or_else( - |_| Targets::new(), - |env_var| { - use std::str::FromStr; - Targets::from_str(&env_var).unwrap_or_default() - }, - ) - // disable brush-parser tracing - .with_targets([("tokenize", LevelFilter::OFF), ("parse", LevelFilter::OFF)]), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); - }); -} - -async fn read_vite_config_from_workspace_root< - ResolveUniversalViteConfig: Future>, - ResolveUniversalViteConfigFn: Fn(String) -> ResolveUniversalViteConfig, ->( - workspace_root: &AbsolutePathBuf, - resolve_universal_vite_config: Option<&ResolveUniversalViteConfigFn>, -) -> Result, Error> { - if let Some(resolve_universal_vite_config) = resolve_universal_vite_config { - let vite_config = - resolve_universal_vite_config(workspace_root.as_path().to_string_lossy().to_string()) - .await?; - return Ok(Some(vite_config)); - } - Ok(None) -} - -/// Check if install args contain packages (non-flag arguments). -/// If packages are detected, reparse as Add command. -fn parse_install_as_add(args: &[String]) -> Option { - // Check if there are any non-flag arguments (potential package names) - let has_packages = args.iter().any(|arg| !arg.starts_with('-')); - - if !has_packages { - return None; - } - - // Reconstruct command line with "add" subcommand - let mut cmd_args = vec!["vite".to_string(), "add".to_string()]; - cmd_args.extend_from_slice(args); - - // Try to parse as Add command - match Args::try_parse_from(&cmd_args) { - Ok(parsed_args) => Some(parsed_args.commands), - Err(_) => None, // If parsing fails, fall back to regular install - } -} - -/// Execute add command with the given parameters -async fn execute_add_command( - cwd: AbsolutePathBuf, - packages: &[String], - save_prod: bool, - save_dev: bool, - save_peer: bool, - save_optional: bool, - save_exact: bool, - save_catalog: bool, - save_catalog_name: Option<&str>, - filter: Option<&[String]>, - workspace_root: bool, - workspace: bool, - global: bool, - allow_build: Option<&str>, - pass_through_args: Option<&[String]>, -) -> Result { - let save_dependency_type = if save_dev { - Some(SaveDependencyType::Dev) - } else if save_peer { - Some(SaveDependencyType::Peer) - } else if save_optional { - Some(SaveDependencyType::Optional) - } else if save_prod { - Some(SaveDependencyType::Production) - } else { - None - }; - - // empty string means save as `catalog:` - let save_catalog_name = if save_catalog { Some("") } else { save_catalog_name }; - - AddCommand::new(cwd) - .execute( - packages, - save_dependency_type, - save_exact, - save_catalog_name, - filter, - workspace_root, - workspace, - global, - allow_build, - pass_through_args, - ) - .await -} - -#[cfg(test)] -mod tests { - use clap::Parser; - - use super::*; - - #[test] - fn test_args_basic_task() { - let args = Args::try_parse_from(["vite-plus", "build"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - assert!(matches!(args.commands, Commands::Build { .. })); - assert!(!args.debug); - } - - #[test] - fn test_args_fmt_command() { - let args = Args::try_parse_from(["vite-plus", "fmt"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - assert!(matches!(args.commands, Commands::Fmt { .. })); - assert!(!args.debug); - } - - #[test] - fn test_args_fmt_command_with_args() { - let args = Args::try_parse_from([ - "vite-plus", - "fmt", - "--", - "--check", - "--ignore-path", - ".gitignore", - ]) - .unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - if let Commands::Fmt { args } = &args.commands { - assert_eq!( - args, - &vec!["--check".to_string(), "--ignore-path".to_string(), ".gitignore".to_string()] - ); - } else { - panic!("Expected Fmt command"); - } - } - - #[test] - fn test_args_test_command() { - let args = Args::try_parse_from(["vite-plus", "test"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - assert!(matches!(args.commands, Commands::Test { .. })); - assert!(!args.debug); - } - - #[test] - fn test_args_test_command_with_args() { - let args = - Args::try_parse_from(["vite-plus", "test", "--", "--watch", "--coverage"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - if let Commands::Test { args } = &args.commands { - assert_eq!(args, &vec!["--watch".to_string(), "--coverage".to_string()]); - } else { - panic!("Expected Test command"); - } - } - - #[test] - fn test_args_lib_command() { - let args = Args::try_parse_from(["vite-plus", "lib"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - assert!(matches!(args.commands, Commands::Lib { .. })); - } - - #[test] - fn test_args_lib_command_with_args() { - let args = Args::try_parse_from(["vite-plus", "lib", "--", "--watch", "--outdir", "dist"]) - .unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - if let Commands::Lib { args } = &args.commands { - assert_eq!( - args, - &vec!["--watch".to_string(), "--outdir".to_string(), "dist".to_string()] - ); - } else { - panic!("Expected Lib command"); - } - } - - #[test] - fn test_args_debug_flag() { - let args = Args::try_parse_from(["vite-plus", "--debug", "build"]).unwrap(); - assert_eq!(args.task, None); - assert!(matches!(args.commands, Commands::Build { .. })); - assert!(args.debug); - } - - #[test] - fn test_args_debug_flag_short() { - let args = Args::try_parse_from(["vite-plus", "-d", "build"]).unwrap(); - assert_eq!(args.task, None); - assert!(matches!(args.commands, Commands::Build { .. })); - assert!(args.debug); - } - - #[test] - fn test_boolean_flag_negation() { - // Test --no-debug alone - let args = Args::try_parse_from(["vite-plus", "--no-debug", "build"]).unwrap(); - assert!(!args.debug); - assert!(args.no_debug); - assert!(!resolve_bool_flag(args.debug, args.no_debug)); - - // Test run command with --no-recursive - let args = Args::try_parse_from(["vite-plus", "run", "--no-recursive", "build"]).unwrap(); - if let Commands::Run { recursive, no_recursive, .. } = args.commands { - assert!(!recursive); - assert!(no_recursive); - assert!(!resolve_bool_flag(recursive, no_recursive)); - } else { - panic!("Expected Run command"); - } - - // Test run command with --no-parallel - let args = Args::try_parse_from(["vite-plus", "run", "--no-parallel", "build"]).unwrap(); - if let Commands::Run { parallel, no_parallel, .. } = args.commands { - assert!(!parallel); - assert!(no_parallel); - assert!(!resolve_bool_flag(parallel, no_parallel)); - } else { - panic!("Expected Run command"); - } - - // Test run command with --no-topological - let args = Args::try_parse_from(["vite-plus", "run", "--no-topological", "build"]).unwrap(); - if let Commands::Run { topological, no_topological, .. } = args.commands { - assert_eq!(topological, None); - assert!(no_topological); - // no_topological takes precedence - assert!(no_topological); - } else { - panic!("Expected Run command"); - } - - // Test --debug vs --no-debug conflict (should fail) - let result = Args::try_parse_from(["vite-plus", "--debug", "--no-debug", "build"]); - assert!(result.is_err()); - - // Test recursive with topological default behavior - let args = Args::try_parse_from(["vite-plus", "run", "--recursive", "build"]).unwrap(); - if let Commands::Run { recursive, no_recursive, topological, no_topological, .. } = - args.commands - { - assert!(recursive); - assert!(!no_recursive); - assert_eq!(topological, None); // Not explicitly set - assert!(!no_topological); - // In the main function, this would default to true for recursive - } else { - panic!("Expected Run command"); - } - - // Test recursive with --no-topological - let args = - Args::try_parse_from(["vite-plus", "run", "--recursive", "--no-topological", "build"]) - .unwrap(); - if let Commands::Run { recursive, no_recursive, topological, no_topological, .. } = - args.commands - { - assert!(recursive); - assert!(!no_recursive); - assert_eq!(topological, None); - assert!(no_topological); - // no_topological should force topological to be false - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_command_basic() { - let args = Args::try_parse_from(["vite-plus", "run", "build", "test"]).unwrap(); - assert!(args.task.is_none()); - - if let Commands::Run { - tasks, - task_args, - recursive, - sequential, - parallel, - topological, - .. - } = args.commands - { - assert_eq!(tasks, vec!["build", "test"]); - assert!(task_args.is_empty()); - assert!(!recursive); - assert!(!sequential); - assert!(!parallel); - assert!(topological.is_none()); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_command_with_flags() { - let args = - Args::try_parse_from(["vite-plus", "run", "--recursive", "--sequential", "build"]) - .unwrap(); - - if let Commands::Run { tasks, recursive, sequential, parallel, .. } = args.commands { - assert_eq!(tasks, vec!["build"]); - assert!(recursive); - assert!(sequential); - assert!(!parallel); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_command_with_parallel_flag() { - let args = - Args::try_parse_from(["vite-plus", "run", "--parallel", "build", "test"]).unwrap(); - - if let Commands::Run { tasks, parallel, sequential, .. } = args.commands { - assert_eq!(tasks, vec!["build", "test"]); - assert!(parallel); - assert!(!sequential); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_command_with_task_args() { - let args = Args::try_parse_from([ - "vite-plus", - "run", - "build", - "test", - "--", - "--watch", - "--verbose", - ]) - .unwrap(); - - if let Commands::Run { tasks, task_args, .. } = args.commands { - assert_eq!(tasks, vec!["build", "test"]); - assert_eq!(task_args, vec!["--watch", "--verbose"]); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_command_all_flags() { - let args = Args::try_parse_from([ - "vite-plus", - "run", - "--recursive", - "--sequential", - "--parallel", - "build", - ]) - .unwrap(); - - if let Commands::Run { tasks, recursive, sequential, parallel, .. } = args.commands { - assert_eq!(tasks, vec!["build"]); - assert!(recursive); - assert!(sequential); - assert!(parallel); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_debug_with_run_command() { - let args = Args::try_parse_from(["vite-plus", "--debug", "run", "build"]).unwrap(); - - assert!(args.debug); - if let Commands::Run { tasks, .. } = args.commands { - assert_eq!(tasks, vec!["build"]); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_short_flags() { - let args = Args::try_parse_from(["vite-plus", "run", "-r", "-s", "-p", "build"]).unwrap(); - - if let Commands::Run { tasks, recursive, sequential, parallel, .. } = args.commands { - assert_eq!(tasks, vec!["build"]); - assert!(recursive); - assert!(sequential); - assert!(parallel); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_run_empty_tasks() { - let args = Args::try_parse_from(["vite-plus", "run"]).unwrap(); - - if let Commands::Run { tasks, .. } = args.commands { - assert!(tasks.is_empty(), "Tasks should be empty when none provided"); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_args_doc_command() { - let args = Args::try_parse_from(["vite-plus", "doc"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - assert!(matches!(args.commands, Commands::Doc { .. })); - assert!(!args.debug); - } - - #[test] - fn test_args_doc_command_with_args() { - let args = - Args::try_parse_from(["vite-plus", "doc", "build", "--host", "0.0.0.0"]).unwrap(); - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - if let Commands::Doc { args } = &args.commands { - assert_eq!( - args, - &vec!["build".to_string(), "--host".to_string(), "0.0.0.0".to_string()] - ); - } else { - panic!("Expected Doc command"); - } - } - - #[test] - fn test_args_complex_task_args() { - let args = Args::try_parse_from([ - "vite-plus", - "test", - "--", - "--config", - "jest.config.js", - "--coverage", - "--watch", - ]) - .unwrap(); - - // "test" is now a dedicated command - assert_eq!(args.task, None); - assert!(args.task_args.is_empty()); - if let Commands::Test { args } = &args.commands { - assert_eq!( - args, - &vec![ - "--config".to_string(), - "jest.config.js".to_string(), - "--coverage".to_string(), - "--watch".to_string() - ] - ); - } else { - panic!("Expected Test command"); - } - } - - #[test] - fn test_args_run_complex_task_args() { - let args = Args::try_parse_from([ - "vite-plus", - "run", - "--recursive", - "build", - "test", - "--", - "--env", - "production", - "--output-dir", - "dist", - ]) - .unwrap(); - - if let Commands::Run { tasks, task_args, recursive, .. } = args.commands { - assert_eq!(tasks, vec!["build", "test"]); - assert_eq!(task_args, vec!["--env", "production", "--output-dir", "dist"]); - assert!(recursive); - } else { - panic!("Expected Run command"); - } - } - - #[test] - fn test_run_command_uses_subcommand_task_args() { - // This test verifies that the main function uses task_args from Commands::Run, - // not from the top-level Args struct - let args1 = Args::try_parse_from([ - "vite-plus", - "run", - "build", - "--", - "--watch", - "--mode=production", - ]) - .unwrap(); - - let args2 = - Args::try_parse_from(["vite-plus", "build", "--", "--watch", "--mode=development"]) - .unwrap(); - - // Verify args1: explicit mode with run subcommand - assert!(args1.task.is_none()); - assert!(args1.task_args.is_empty()); // Top-level task_args should be empty - if let Commands::Run { tasks, task_args, .. } = &args1.commands { - assert_eq!(tasks, &vec!["build"]); - assert_eq!(task_args, &vec!["--watch", "--mode=production"]); - } else { - panic!("Expected Run command"); - } - - // Verify args2: now maps to Build command instead of implicit mode - assert_eq!(args2.task, None); - assert!(args2.task_args.is_empty()); // Build command captures args directly, not via task_args - if let Commands::Build { args } = &args2.commands { - assert_eq!(args, &vec!["--watch".to_string(), "--mode=development".to_string()]); - } else { - panic!("Expected Build command"); - } - } - - #[tokio::test] - async fn test_auto_install_skipped_conditions() { - use vite_path::AbsolutePathBuf; - - // Test auto_install function directly - let test_workspace = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test-workspace-not-exists".into()).unwrap() - } else { - AbsolutePathBuf::new("/test-workspace-not-exists".into()).unwrap() - }; - - // Without the environment variable, auto_install should attempt to run - // (it may fail due to invalid workspace, but that's expected) - unsafe { - std::env::remove_var("VITE_TASK_EXECUTION_ENV"); - } - let result_without_env = auto_install(&test_workspace).await; - // Should attempt to run (and likely fail with workspace error, which is fine) - assert!(result_without_env.is_err()); - - // With environment variable set to different value, auto_install should still attempt to run - unsafe { - std::env::set_var("VITE_TASK_EXECUTION_ENV", "0"); - } - let result_with_wrong_value = auto_install(&test_workspace).await; - // Should attempt to run (and likely fail with workspace error, which is fine) - assert!(result_with_wrong_value.is_err()); - - // With environment variable set to "1", auto_install should be skipped (return Ok) - unsafe { - std::env::set_var("VITE_TASK_EXECUTION_ENV", "1"); - } - let result_with_correct_value = auto_install(&test_workspace).await; - assert!(result_with_correct_value.is_ok()); - - // Clean up - unsafe { - std::env::remove_var("VITE_TASK_EXECUTION_ENV"); - } - } - - mod install_as_add_tests { - use super::*; - - #[test] - fn test_parse_install_as_add_with_packages() { - let args = vec!["react".to_string(), "react-dom".to_string()]; - let result = parse_install_as_add(&args); - assert!(result.is_some()); - if let Some(Commands::Add { packages, save_dev, save_exact, .. }) = result { - assert_eq!(packages, vec!["react", "react-dom"]); - assert!(!save_dev); - assert!(!save_exact); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_parse_install_as_add_with_dev_flag() { - let args = vec!["-D".to_string(), "typescript".to_string()]; - let result = parse_install_as_add(&args); - assert!(result.is_some()); - if let Some(Commands::Add { packages, save_dev, .. }) = result { - assert_eq!(packages, vec!["typescript"]); - assert!(save_dev); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_parse_install_as_add_without_packages() { - let args = vec![]; - let result = parse_install_as_add(&args); - assert!(result.is_none()); - } - - #[test] - fn test_parse_install_as_add_with_only_flags() { - let args = vec!["--some-install-flag".to_string()]; - let result = parse_install_as_add(&args); - assert!(result.is_none()); - } - - #[test] - fn test_parse_install_as_add_complex() { - let args = vec![ - "-D".to_string(), - "-E".to_string(), - "--filter".to_string(), - "app".to_string(), - "typescript".to_string(), - "eslint".to_string(), - ]; - let result = parse_install_as_add(&args); - assert!(result.is_some()); - if let Some(Commands::Add { packages, save_dev, save_exact, filter, .. }) = result { - assert_eq!(packages, vec!["typescript", "eslint"]); - assert!(save_dev); - assert!(save_exact); - assert_eq!(filter.unwrap(), vec!["app"]); - } else { - panic!("Expected Add command"); - } - } - } - - mod add_command_tests { - use super::*; - - #[test] - fn test_args_add_command() { - let args = Args::try_parse_from(&["vite-plus", "add", "react"]).unwrap(); - if let Commands::Add { filter, workspace_root, workspace, packages, .. } = - &args.commands - { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(filter.is_none()); - assert!(!workspace_root); - assert!(!workspace); - } else { - panic!("Expected Add command"); - } - - let args = Args::try_parse_from(&["vite-plus", "add", "--save-peer", "react"]).unwrap(); - if let Commands::Add { - filter, workspace_root, workspace, packages, save_peer, .. - } = &args.commands - { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(filter.is_none()); - assert!(!workspace_root); - assert!(!workspace); - assert!(save_peer); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_args_add_command_with_workspace_root() { - let args = Args::try_parse_from(&["vite-plus", "add", "-w", "react"]).unwrap(); - if let Commands::Add { filter, workspace_root, workspace, packages, .. } = - &args.commands - { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(filter.is_none()); - assert!(workspace_root); - assert!(!workspace); - } else { - panic!("Expected Add command"); - } - let args = Args::try_parse_from(&["vite-plus", "add", "react", "-w"]).unwrap(); - if let Commands::Add { filter, workspace_root, workspace, packages, .. } = - &args.commands - { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(filter.is_none()); - assert!(workspace_root); - assert!(!workspace); - } else { - panic!("Expected Add command"); - } - - let args = - Args::try_parse_from(&["vite-plus", "add", "react", "--workspace-root"]).unwrap(); - if let Commands::Add { filter, workspace_root, workspace, packages, .. } = - &args.commands - { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(filter.is_none()); - assert!(workspace_root); - assert!(!workspace); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_args_add_command_multiple_packages() { - let args = - Args::try_parse_from(&["vite-plus", "add", "react", "react-dom", "@types/react"]) - .unwrap(); - if let Commands::Add { packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react", "react-dom", "@types/react"]); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_args_add_command_with_flags() { - let args = Args::try_parse_from(&[ - "vite-plus", - "add", - "--filter", - "app", - "-w", - "--workspace", - "typescript", - "-D", - ]) - .unwrap(); - if let Commands::Add { filter, workspace_root, workspace, packages, save_dev, .. } = - &args.commands - { - assert_eq!(filter, &Some(vec!["app".to_string()])); - assert!(workspace_root); - assert!(workspace); - assert_eq!(packages, &vec!["typescript"]); - assert!(save_dev); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_args_add_command_with_allow_build() { - let args = Args::try_parse_from(&[ - "vite-plus", - "add", - "--filter", - "app", - "-w", - "--workspace", - "typescript", - "-D", - "--allow-build=react,napi", - ]) - .unwrap(); - if let Commands::Add { - filter, - workspace_root, - workspace, - packages, - save_dev, - allow_build, - .. - } = &args.commands - { - assert_eq!(filter, &Some(vec!["app".to_string()])); - assert!(workspace_root); - assert!(workspace); - assert_eq!(packages, &vec!["typescript"]); - assert!(save_dev); - assert_eq!(allow_build, &Some("react,napi".to_string())); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_args_add_command_multiple_filters() { - let args = Args::try_parse_from(&[ - "vite-plus", - "add", - "--filter", - "app", - "--filter", - "web", - "react", - ]) - .unwrap(); - if let Commands::Add { filter, packages, .. } = &args.commands { - assert_eq!(filter, &Some(vec!["app".to_string(), "web".to_string()])); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Add command"); - } - } - - #[test] - fn test_args_add_command_invalid_filter() { - let args = Args::try_parse_from(&["vite-plus", "add", "react", "--filter"]); - assert!(args.is_err()); - } - - #[test] - fn test_args_add_command_with_pass_through_args() { - let args = Args::try_parse_from(&[ - "vite-plus", - "add", - "react", - "--", - "--watch", - "--mode=production", - "--use-stderr", - ]) - .unwrap(); - if let Commands::Add { packages, pass_through_args, .. } = &args.commands { - assert_eq!(packages, &vec!["react"]); - assert_eq!( - pass_through_args, - &Some(vec![ - "--watch".to_string(), - "--mode=production".to_string(), - "--use-stderr".to_string() - ]) - ); - } else { - panic!("Expected Add command"); - } - - let args = Args::try_parse_from(&[ - "vite-plus", - "add", - "react", - "napi", - "--", - "--allow-build=react,napi", - ]) - .unwrap(); - if let Commands::Add { packages, pass_through_args, .. } = &args.commands { - assert_eq!(packages, &vec!["react", "napi"]); - assert_eq!(pass_through_args, &Some(vec!["--allow-build=react,napi".to_string()])); - } else { - panic!("Expected Add command"); - } - } - } - - mod remove_command_tests { - use super::*; - - #[test] - fn test_args_remove_command() { - let args = Args::try_parse_from(&["vite-plus", "remove", "react"]).unwrap(); - if let Commands::Remove { - save_dev, - save_optional, - save_prod, - filter, - workspace_root, - recursive, - global, - packages, - pass_through_args, - } = &args.commands - { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(!save_dev); - assert!(!save_optional); - assert!(!save_prod); - assert!(filter.is_none()); - assert!(!workspace_root); - assert!(!recursive); - assert!(!global); - assert!(pass_through_args.is_none()); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_dev_flag() { - let args = Args::try_parse_from(&["vite-plus", "remove", "-D", "typescript"]).unwrap(); - if let Commands::Remove { save_dev, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["typescript".to_string()]); - assert!(save_dev); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_optional_flag() { - let args = Args::try_parse_from(&["vite-plus", "remove", "-O", "lodash"]).unwrap(); - if let Commands::Remove { save_optional, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["lodash".to_string()]); - assert!(save_optional); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_prod_flag() { - let args = Args::try_parse_from(&["vite-plus", "remove", "-P", "express"]).unwrap(); - if let Commands::Remove { save_prod, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["express".to_string()]); - assert!(save_prod); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_workspace_root() { - let args = Args::try_parse_from(&["vite-plus", "remove", "-w", "react"]).unwrap(); - if let Commands::Remove { workspace_root, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(workspace_root); - } else { - panic!("Expected Remove command"); - } - - let args = Args::try_parse_from(&["vite-plus", "remove", "react", "--workspace-root"]) - .unwrap(); - if let Commands::Remove { workspace_root, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(workspace_root); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_recursive() { - let args = Args::try_parse_from(&["vite-plus", "remove", "-r", "react"]).unwrap(); - if let Commands::Remove { recursive, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(recursive); - } else { - panic!("Expected Remove command"); - } - - let args = - Args::try_parse_from(&["vite-plus", "remove", "react", "--recursive"]).unwrap(); - if let Commands::Remove { recursive, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react".to_string()]); - assert!(recursive); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_global() { - let args = Args::try_parse_from(&["vite-plus", "remove", "-g", "npm"]).unwrap(); - if let Commands::Remove { global, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["npm".to_string()]); - assert!(global); - } else { - panic!("Expected Remove command"); - } - - let args = Args::try_parse_from(&["vite-plus", "remove", "npm", "--global"]).unwrap(); - if let Commands::Remove { global, packages, .. } = &args.commands { - assert_eq!(packages, &vec!["npm".to_string()]); - assert!(global); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_multiple_packages() { - let args = Args::try_parse_from(&[ - "vite-plus", - "remove", - "react", - "react-dom", - "@types/react", - ]) - .unwrap(); - if let Commands::Remove { packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react", "react-dom", "@types/react"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_single_filter() { - let args = - Args::try_parse_from(&["vite-plus", "remove", "--filter", "app", "typescript"]) - .unwrap(); - if let Commands::Remove { filter, packages, .. } = &args.commands { - assert_eq!(filter, &Some(vec!["app".to_string()])); - assert_eq!(packages, &vec!["typescript"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_multiple_filters() { - let args = Args::try_parse_from(&[ - "vite-plus", - "remove", - "--filter", - "app", - "--filter", - "web", - "react", - ]) - .unwrap(); - if let Commands::Remove { filter, packages, .. } = &args.commands { - assert_eq!(filter, &Some(vec!["app".to_string(), "web".to_string()])); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_combined_flags() { - let args = Args::try_parse_from(&[ - "vite-plus", - "remove", - "-D", - "-w", - "--filter", - "app", - "typescript", - "eslint", - ]) - .unwrap(); - if let Commands::Remove { save_dev, workspace_root, filter, packages, .. } = - &args.commands - { - assert!(save_dev); - assert!(workspace_root); - assert_eq!(filter, &Some(vec!["app".to_string()])); - assert_eq!(packages, &vec!["typescript", "eslint"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_with_pass_through_args() { - let args = Args::try_parse_from(&[ - "vite-plus", - "remove", - "react", - "--", - "--ignore-scripts", - "--force", - ]) - .unwrap(); - if let Commands::Remove { packages, pass_through_args, .. } = &args.commands { - assert_eq!(packages, &vec!["react"]); - assert_eq!( - pass_through_args, - &Some(vec!["--ignore-scripts".to_string(), "--force".to_string()]) - ); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_alias_rm() { - let args = Args::try_parse_from(&["vite-plus", "rm", "react"]).unwrap(); - if let Commands::Remove { packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_alias_un() { - let args = Args::try_parse_from(&["vite-plus", "un", "react"]).unwrap(); - if let Commands::Remove { packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_alias_uninstall() { - let args = Args::try_parse_from(&["vite-plus", "uninstall", "react"]).unwrap(); - if let Commands::Remove { packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Remove command"); - } - } - - #[test] - fn test_args_remove_command_invalid_filter() { - let args = Args::try_parse_from(&["vite-plus", "remove", "react", "--filter"]); - assert!(args.is_err()); - } - - #[test] - fn test_args_remove_command_complex_scenario() { - let args = Args::try_parse_from(&[ - "vite-plus", - "remove", - "-D", - "-r", - "--filter", - "app", - "--filter", - "web", - "typescript", - "eslint", - "@types/node", - "--", - "--ignore-scripts", - ]) - .unwrap(); - if let Commands::Remove { - save_dev, - recursive, - filter, - packages, - pass_through_args, - .. - } = &args.commands - { - assert!(save_dev); - assert!(recursive); - assert_eq!(filter, &Some(vec!["app".to_string(), "web".to_string()])); - assert_eq!(packages, &vec!["typescript", "eslint", "@types/node"]); - assert_eq!(pass_through_args, &Some(vec!["--ignore-scripts".to_string()])); - } else { - panic!("Expected Remove command"); - } - } - } - - mod update_command_tests { - use super::*; - - #[test] - fn test_args_update_command_basic() { - let args = Args::try_parse_from(&["vite-plus", "update"]).unwrap(); - if let Commands::Update { - latest, - global, - recursive, - filter, - workspace_root, - dev, - prod, - interactive, - no_optional, - no_save, - workspace, - packages, - .. - } = &args.commands - { - assert!(!latest); - assert!(!global); - assert!(!recursive); - assert!(filter.is_none()); - assert!(!workspace_root); - assert!(!dev); - assert!(!prod); - assert!(!interactive); - assert!(!no_optional); - assert!(!no_save); - assert!(!workspace); - assert!(packages.is_empty()); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_alias() { - let args = Args::try_parse_from(&["vite-plus", "up"]).unwrap(); - assert!(matches!(args.commands, Commands::Update { .. })); - } - - #[test] - fn test_args_update_command_with_packages() { - let args = - Args::try_parse_from(&["vite-plus", "update", "react", "react-dom"]).unwrap(); - if let Commands::Update { packages, .. } = &args.commands { - assert_eq!(packages, &vec!["react", "react-dom"]); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_latest_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-L", "react"]).unwrap(); - if let Commands::Update { latest, packages, .. } = &args.commands { - assert!(latest); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--latest", "react"]).unwrap(); - if let Commands::Update { latest, packages, .. } = &args.commands { - assert!(latest); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_global_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-g"]).unwrap(); - if let Commands::Update { global, .. } = &args.commands { - assert!(global); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--global"]).unwrap(); - if let Commands::Update { global, .. } = &args.commands { - assert!(global); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_recursive_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-r"]).unwrap(); - if let Commands::Update { recursive, .. } = &args.commands { - assert!(recursive); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--recursive"]).unwrap(); - if let Commands::Update { recursive, .. } = &args.commands { - assert!(recursive); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_workspace_root_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-w"]).unwrap(); - if let Commands::Update { workspace_root, .. } = &args.commands { - assert!(workspace_root); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--workspace-root"]).unwrap(); - if let Commands::Update { workspace_root, .. } = &args.commands { - assert!(workspace_root); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_dev_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-D"]).unwrap(); - if let Commands::Update { dev, .. } = &args.commands { - assert!(dev); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--dev"]).unwrap(); - if let Commands::Update { dev, .. } = &args.commands { - assert!(dev); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_prod_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-P"]).unwrap(); - if let Commands::Update { prod, .. } = &args.commands { - assert!(prod); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--prod"]).unwrap(); - if let Commands::Update { prod, .. } = &args.commands { - assert!(prod); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_interactive_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "-i"]).unwrap(); - if let Commands::Update { interactive, .. } = &args.commands { - assert!(interactive); - } else { - panic!("Expected Update command"); - } - - let args = Args::try_parse_from(&["vite-plus", "update", "--interactive"]).unwrap(); - if let Commands::Update { interactive, .. } = &args.commands { - assert!(interactive); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_no_optional_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "--no-optional"]).unwrap(); - if let Commands::Update { no_optional, .. } = &args.commands { - assert!(no_optional); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_no_save_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "--no-save"]).unwrap(); - if let Commands::Update { no_save, .. } = &args.commands { - assert!(no_save); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_workspace_flag() { - let args = Args::try_parse_from(&["vite-plus", "update", "--workspace"]).unwrap(); - if let Commands::Update { workspace, .. } = &args.commands { - assert!(workspace); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_filter() { - let args = - Args::try_parse_from(&["vite-plus", "update", "--filter", "app", "react"]).unwrap(); - if let Commands::Update { filter, packages, .. } = &args.commands { - assert_eq!(filter, &Some(vec!["app".to_string()])); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_multiple_filters() { - let args = Args::try_parse_from(&[ - "vite-plus", - "update", - "--filter", - "app", - "--filter", - "web", - "react", - ]) - .unwrap(); - if let Commands::Update { filter, packages, .. } = &args.commands { - assert_eq!(filter, &Some(vec!["app".to_string(), "web".to_string()])); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_combined_flags() { - let args = Args::try_parse_from(&[ - "vite-plus", - "update", - "-L", - "-r", - "-D", - "--filter", - "app", - "typescript", - "eslint", - ]) - .unwrap(); - if let Commands::Update { latest, recursive, dev, filter, packages, .. } = - &args.commands - { - assert!(latest); - assert!(recursive); - assert!(dev); - assert_eq!(filter, &Some(vec!["app".to_string()])); - assert_eq!(packages, &vec!["typescript", "eslint"]); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_with_pass_through_args() { - let args = Args::try_parse_from(&[ - "vite-plus", - "update", - "react", - "--", - "--registry", - "https://custom-registry.com", - ]) - .unwrap(); - if let Commands::Update { packages, pass_through_args, .. } = &args.commands { - assert_eq!(packages, &vec!["react"]); - assert_eq!( - pass_through_args, - &Some(vec![ - "--registry".to_string(), - "https://custom-registry.com".to_string() - ]) - ); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_complex_scenario() { - let args = Args::try_parse_from(&[ - "vite-plus", - "update", - "-L", - "-r", - "-w", - "-D", - "--filter", - "app", - "--filter", - "web", - "--no-optional", - "react", - "vue", - "--", - "--registry", - "https://registry.npmjs.org", - ]) - .unwrap(); - if let Commands::Update { - latest, - recursive, - workspace_root, - dev, - filter, - no_optional, - packages, - pass_through_args, - .. - } = &args.commands - { - assert!(latest); - assert!(recursive); - assert!(workspace_root); - assert!(dev); - assert_eq!(filter, &Some(vec!["app".to_string(), "web".to_string()])); - assert!(no_optional); - assert_eq!(packages, &vec!["react", "vue"]); - assert_eq!( - pass_through_args, - &Some(vec!["--registry".to_string(), "https://registry.npmjs.org".to_string()]) - ); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_all_packages() { - // When no packages are specified, should update all packages - let args = Args::try_parse_from(&["vite-plus", "update", "-r"]).unwrap(); - if let Commands::Update { recursive, packages, .. } = &args.commands { - assert!(recursive); - assert!(packages.is_empty()); - } else { - panic!("Expected Update command"); - } - } - - #[test] - fn test_args_update_command_workspace_combinations() { - // Test --workspace-root with --recursive - let args = Args::try_parse_from(&["vite-plus", "update", "-w", "-r"]).unwrap(); - if let Commands::Update { workspace_root, recursive, .. } = &args.commands { - assert!(workspace_root); - assert!(recursive); - } else { - panic!("Expected Update command"); - } - - // Test --workspace flag - let args = - Args::try_parse_from(&["vite-plus", "update", "--workspace", "react"]).unwrap(); - if let Commands::Update { workspace, packages, .. } = &args.commands { - assert!(workspace); - assert_eq!(packages, &vec!["react"]); - } else { - panic!("Expected Update command"); - } - } - } - - mod dedupe_command_tests { - use super::*; - - #[test] - fn test_args_dedupe_command_basic() { - let args = Args::try_parse_from(&["vite-plus", "dedupe"]).unwrap(); - if let Commands::Dedupe { check, .. } = &args.commands { - assert!(!check); - } else { - panic!("Expected Dedupe command"); - } - } - - #[test] - fn test_args_dedupe_command_with_alias() { - let args = Args::try_parse_from(&["vite-plus", "ddp"]).unwrap(); - assert!(matches!(args.commands, Commands::Dedupe { .. })); - } - - #[test] - fn test_args_dedupe_command_with_check() { - let args = Args::try_parse_from(&["vite-plus", "dedupe", "--check"]).unwrap(); - if let Commands::Dedupe { check, .. } = &args.commands { - assert!(check); - } else { - panic!("Expected Dedupe command"); - } - } - - #[test] - fn test_args_dedupe_command_with_pass_through_args() { - let args = Args::try_parse_from(&[ - "vite-plus", - "dedupe", - "--", - "--some-flag", - "--another-flag", - ]) - .unwrap(); - if let Commands::Dedupe { pass_through_args, .. } = &args.commands { - assert_eq!( - pass_through_args, - &Some(vec!["--some-flag".to_string(), "--another-flag".to_string()]) - ); - } else { - panic!("Expected Dedupe command"); - } - } - - #[test] - fn test_args_dedupe_command_with_check_and_pass_through() { - let args = - Args::try_parse_from(&["vite-plus", "dedupe", "--check", "--", "--custom-flag"]) - .unwrap(); - if let Commands::Dedupe { check, pass_through_args, .. } = &args.commands { - assert!(check); - assert_eq!(pass_through_args, &Some(vec!["--custom-flag".to_string()])); - } else { - panic!("Expected Dedupe command"); - } - } - } -} diff --git a/packages/cli/binding/src/commands/add.rs b/packages/cli/binding/src/commands/add.rs deleted file mode 100644 index 568d31f5..00000000 --- a/packages/cli/binding/src/commands/add.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{ - commands::add::{AddCommandOptions, SaveDependencyType}, - package_manager::PackageManager, -}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Add command for adding packages to dependencies. -/// -/// This command automatically detects the package manager and translates -/// the add command to the appropriate package manager-specific syntax. -pub struct AddCommand { - cwd: AbsolutePathBuf, -} - -impl AddCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - packages: &[String], - save_dependency_type: Option, - save_exact: bool, - save_catalog_name: Option<&str>, - filters: Option<&[String]>, - workspace_root: bool, - workspace_only: bool, - global: bool, - allow_build: Option<&str>, - pass_through_args: Option<&[String]>, - ) -> Result { - let add_command_options = AddCommandOptions { - packages, - save_dependency_type, - save_exact, - filters, - workspace_root, - workspace_only, - global, - save_catalog_name, - allow_build, - pass_through_args, - }; - - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - package_manager.run_add_command(&add_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_add_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = AddCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/dedupe.rs b/packages/cli/binding/src/commands/dedupe.rs deleted file mode 100644 index 5be3b920..00000000 --- a/packages/cli/binding/src/commands/dedupe.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{commands::dedupe::DedupeCommandOptions, package_manager::PackageManager}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Dedupe command for deduplicating dependencies by removing older versions. -/// -/// This command automatically detects the package manager and translates -/// the dedupe command to the appropriate package manager-specific syntax. -pub struct DedupeCommand { - cwd: AbsolutePathBuf, -} - -impl DedupeCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - check: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let dedupe_command_options = DedupeCommandOptions { check, pass_through_args }; - package_manager.run_dedupe_command(&dedupe_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dedupe_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = DedupeCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/doc.rs b/packages/cli/binding/src/commands/doc.rs deleted file mode 100644 index 2a7de7b6..00000000 --- a/packages/cli/binding/src/commands/doc.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::future::Future; - -use petgraph::stable_graph::StableGraph; -use vite_error::Error as ViteError; -use vite_task::{ - Error, ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace, -}; - -pub async fn doc< - Doc: Future>, - DocFn: Fn() -> Doc, ->( - resolve_doc_command: DocFn, - workspace: &Workspace, - args: &Vec, -) -> Result { - let wrapped_command = - || async { resolve_doc_command().await.map_err(|e| Error::Anyhow(e.into())) }; - let resolved_task = - ResolvedTask::resolve_from_builtin(workspace, wrapped_command, "doc", args.iter()).await?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - ExecutionPlan::plan(task_graph, false)?.execute(workspace).await -} diff --git a/packages/cli/binding/src/commands/fmt.rs b/packages/cli/binding/src/commands/fmt.rs deleted file mode 100644 index 13122e06..00000000 --- a/packages/cli/binding/src/commands/fmt.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::{collections::HashMap, future::Future}; - -use petgraph::stable_graph::StableGraph; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use vite_error::Error as ViteError; -use vite_task::{ - Error, ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FmtConfig { - pub rules: HashMap, -} - -#[tracing::instrument(skip(resolve_fmt_command, workspace))] -pub async fn fmt< - Fmt: Future>, - FmtFn: Fn() -> Fmt, ->( - resolve_fmt_command: FmtFn, - workspace: &Workspace, - args: &Vec, -) -> Result { - let wrapped_command = - || async { resolve_fmt_command().await.map_err(|e| Error::Anyhow(e.into())) }; - let resolved_task = - ResolvedTask::resolve_from_builtin(workspace, wrapped_command, "fmt", args.iter()).await?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - ExecutionPlan::plan(task_graph, false)?.execute(workspace).await -} diff --git a/packages/cli/binding/src/commands/install.rs b/packages/cli/binding/src/commands/install.rs deleted file mode 100644 index 781cf22d..00000000 --- a/packages/cli/binding/src/commands/install.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::{ - env, - io::{self, IsTerminal, Write}, -}; - -use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyEvent}, - execute, - style::{Color, Print, ResetColor, SetForegroundColor}, - terminal, -}; -use petgraph::stable_graph::StableGraph; -use vite_error::Error; -use vite_install::{PackageManager, PackageManagerType}; -use vite_path::AbsolutePathBuf; -use vite_task::{ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace}; - -/// Install command. -/// -/// This is the command that will be executed by the `vite-plus install` command. -/// -pub struct InstallCommand { - workspace_root: AbsolutePathBuf, - ignore_replay: bool, -} - -/// Install command builder. -/// -/// This is a builder pattern for the `vite-plus install` command. -/// -pub struct InstallCommandBuilder { - workspace_root: AbsolutePathBuf, - ignore_replay: bool, -} - -impl InstallCommand { - pub const fn builder(workspace_root: AbsolutePathBuf) -> InstallCommandBuilder { - InstallCommandBuilder::new(workspace_root) - } - - pub async fn execute(self, args: &Vec) -> Result { - // Handle UnrecognizedPackageManager error and let user select a package manager - let package_manager = match PackageManager::builder(&self.workspace_root).build().await { - Ok(pm) => pm, - Err(Error::UnrecognizedPackageManager) => { - // Prompt user to select a package manager - let selected_type = prompt_package_manager_selection()?; - PackageManager::builder(&self.workspace_root) - .package_manager_type(selected_type) - .build() - .await? - } - Err(e) => return Err(e), - }; - let workspace = Workspace::partial_load(self.workspace_root)?; - let resolve_command = package_manager.resolve_install_command(args); - let resolved_task = ResolvedTask::resolve_from_builtin_with_command_result( - &workspace, - "install", - resolve_command.args.iter(), - ResolveCommandResult { bin_path: resolve_command.bin_path, envs: resolve_command.envs }, - self.ignore_replay, - Some(package_manager.get_fingerprint_ignores()), - )?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - let summary = ExecutionPlan::plan(task_graph, false)?.execute(&workspace).await?; - workspace.unload().await?; - - Ok(summary) - } -} - -impl InstallCommandBuilder { - pub const fn new(workspace_root: AbsolutePathBuf) -> Self { - Self { workspace_root, ignore_replay: false } - } - - pub const fn ignore_replay(mut self) -> Self { - self.ignore_replay = true; - self - } - - pub fn build(self) -> InstallCommand { - InstallCommand { workspace_root: self.workspace_root, ignore_replay: self.ignore_replay } - } -} - -/// Common CI environment variables -const CI_ENV_VARS: &[&str] = &[ - "CI", - "CONTINUOUS_INTEGRATION", - "GITHUB_ACTIONS", - "GITLAB_CI", - "CIRCLECI", - "TRAVIS", - "JENKINS_URL", - "BUILDKITE", - "DRONE", - "CODEBUILD_BUILD_ID", // AWS CodeBuild - "TF_BUILD", // Azure Pipelines -]; - -/// Check if running in a CI environment -fn is_ci_environment() -> bool { - CI_ENV_VARS.iter().any(|key| env::var(key).is_ok()) -} - -/// Interactive menu for selecting a package manager with keyboard navigation -fn interactive_package_manager_menu() -> Result { - let options = [ - ("pnpm (recommended)", PackageManagerType::Pnpm), - ("npm", PackageManagerType::Npm), - ("yarn", PackageManagerType::Yarn), - ]; - - let mut selected_index = 0; - - // Print header and instructions with proper line breaks - println!("\n📦 No package manager detected. Please select one:"); - println!( - " Use ↑↓ arrows to navigate, Enter to select, 1-{} for quick selection", - options.len() - ); - println!(" Press Esc, q, or Ctrl+C to cancel installation\n"); - - // Enable raw mode for keyboard input - terminal::enable_raw_mode()?; - - // Clear the selection area and hide cursor - execute!(io::stdout(), cursor::Hide)?; - - let result = loop { - // Display menu with current selection - for (i, (name, _)) in options.iter().enumerate() { - execute!(io::stdout(), cursor::MoveToColumn(2))?; - - if i == selected_index { - // Highlight selected item - execute!( - io::stdout(), - SetForegroundColor(Color::Cyan), - Print("▶ "), - Print(format!("[{}] ", i + 1)), - Print(name), - ResetColor, - Print(" ← ") - )?; - } else { - execute!( - io::stdout(), - Print(" "), - SetForegroundColor(Color::DarkGrey), - Print(format!("[{}] ", i + 1)), - ResetColor, - Print(name), - Print(" ") - )?; - } - - if i < options.len() - 1 { - execute!(io::stdout(), Print("\n"))?; - } - } - - // Move cursor back up for next iteration - if options.len() > 1 { - execute!(io::stdout(), cursor::MoveUp((options.len() - 1) as u16))?; - } - - // Read keyboard input - if let Event::Key(KeyEvent { code, modifiers, .. }) = event::read()? { - match code { - // Handle Ctrl+C for exit - KeyCode::Char('c') if modifiers.contains(event::KeyModifiers::CONTROL) => { - // Clean up terminal before exiting - terminal::disable_raw_mode()?; - execute!( - io::stdout(), - cursor::Show, - cursor::MoveDown(options.len() as u16), - Print("\n\n"), - SetForegroundColor(Color::Yellow), - Print("⚠ Installation cancelled by user\n"), - ResetColor - )?; - return Err(Error::UserCancelled); - } - KeyCode::Up => { - selected_index = selected_index.saturating_sub(1); - } - KeyCode::Down => { - if selected_index < options.len() - 1 { - selected_index += 1; - } - } - KeyCode::Enter | KeyCode::Char(' ') => { - break Ok(options[selected_index].1); - } - KeyCode::Char('1') => { - break Ok(options[0].1); - } - KeyCode::Char('2') if options.len() > 1 => { - break Ok(options[1].1); - } - KeyCode::Char('3') if options.len() > 2 => { - break Ok(options[2].1); - } - KeyCode::Esc | KeyCode::Char('q') => { - // Exit on escape/quit - terminal::disable_raw_mode()?; - execute!( - io::stdout(), - cursor::Show, - cursor::MoveDown(options.len() as u16), - Print("\n\n"), - SetForegroundColor(Color::Yellow), - Print("⚠ Installation cancelled by user\n"), - ResetColor - )?; - return Err(Error::UserCancelled); - } - _ => {} - } - } - }; - - // Clean up: disable raw mode and show cursor - terminal::disable_raw_mode()?; - execute!(io::stdout(), cursor::Show, cursor::MoveDown(options.len() as u16), Print("\n"))?; - - // Print selection confirmation - if let Ok(pm) = &result { - let name = match pm { - PackageManagerType::Pnpm => "pnpm", - PackageManagerType::Npm => "npm", - PackageManagerType::Yarn => "yarn", - }; - println!("\n✓ Selected package manager: {name}\n"); - } - - result -} - -/// Prompt the user to select a package manager -fn prompt_package_manager_selection() -> Result { - // In CI environment, automatically use pnpm without prompting - if is_ci_environment() { - println!("CI environment detected. Using default package manager: pnpm"); - return Ok(PackageManagerType::Pnpm); - } - - // Check if stdin is a TTY (terminal) - if not, use default - if !io::stdin().is_terminal() { - println!("Non-interactive environment detected. Using default package manager: pnpm"); - return Ok(PackageManagerType::Pnpm); - } - - // Try interactive menu first, fall back to simple prompt on error - match interactive_package_manager_menu() { - Ok(pm) => Ok(pm), - Err(err) => { - match err { - Error::UserCancelled => Err(err), - // Fallback to simple text prompt if interactive menu fails - _ => simple_text_prompt(), - } - } - } -} - -/// Simple text-based prompt as fallback -fn simple_text_prompt() -> Result { - let managers = [ - ("pnpm", PackageManagerType::Pnpm), - ("npm", PackageManagerType::Npm), - ("yarn", PackageManagerType::Yarn), - ]; - - println!("\nNo package manager detected. Please select one:"); - println!("────────────────────────────────────────────────"); - - for (i, (name, _)) in managers.iter().enumerate() { - if i == 0 { - println!(" [{}] {} (recommended)", i + 1, name); - } else { - println!(" [{}] {}", i + 1, name); - } - } - - print!("\nEnter your choice (1-{}) [default: 1]: ", managers.len()); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - let choice = input.trim(); - let index = if choice.is_empty() { - 0 // Default to pnpm - } else { - choice - .parse::() - .ok() - .and_then(|n| if n > 0 && n <= managers.len() { Some(n - 1) } else { None }) - .unwrap_or(0) // Default to pnpm if invalid input - }; - - let (name, selected_type) = &managers[index]; - println!("✓ Selected package manager: {name}\n"); - - Ok(*selected_type) -} - -#[cfg(test)] -mod tests { - use std::{fs, path::PathBuf}; - - use serial_test::serial; - use tempfile::TempDir; - - use super::*; - - /// Helper struct to safely manage environment variables in tests - /// This struct ensures that environment variables are properly restored - /// after the test completes, even if the test panics. - struct EnvGuard { - key: String, - original_value: Option, - } - - impl EnvGuard { - fn new(key: &str, value: &str) -> Self { - let original_value = env::var(key).ok(); - // SAFETY: This is only used in tests which are run serially, - // preventing data races on environment variables - unsafe { - env::set_var(key, value); - } - Self { key: key.to_string(), original_value } - } - } - - impl Drop for EnvGuard { - fn drop(&mut self) { - // SAFETY: This is only used in tests which are run serially, - // preventing data races on environment variables - unsafe { - match &self.original_value { - Some(value) => env::set_var(&self.key, value), - None => env::remove_var(&self.key), - } - } - } - } - - #[test] - fn test_install_command_builder_build() { - let workspace_root = AbsolutePathBuf::new(PathBuf::from(if cfg!(windows) { - "C:\\test\\workspace" - } else { - "/test/workspace" - })) - .unwrap(); - let command = InstallCommandBuilder::new(workspace_root.clone()).build(); - - assert_eq!(command.workspace_root, workspace_root); - } - - #[ignore = "skip this test for auto run, should be run manually, because it will prompt for user selection"] - #[tokio::test] - async fn test_install_command_with_package_json_without_package_manager() { - let temp_dir = TempDir::new().unwrap(); - let workspace_root = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Create a minimal package.json - let package_json = r#"{ - "name": "test-package", - "version": "1.0.0" - }"#; - fs::write(workspace_root.join("package.json"), package_json).unwrap(); - - let command = InstallCommandBuilder::new(workspace_root).build(); - assert!(command.execute(&vec![]).await.is_ok()); - } - - #[tokio::test] - #[cfg(not(windows))] // FIXME - async fn test_install_command_with_package_json_with_package_manager() { - let temp_dir = TempDir::new().unwrap(); - let workspace_root = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Create a minimal package.json - let package_json = r#"{ - "name": "test-package", - "version": "1.0.0", - "packageManager": "pnpm@10.15.0" - }"#; - fs::write(workspace_root.join("package.json"), package_json).unwrap(); - - let command = InstallCommandBuilder::new(workspace_root).build(); - let result = command.execute(&vec![]).await; - println!("result: {result:?}"); - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_install_command_execute_with_invalid_workspace() { - let temp_dir = TempDir::new().unwrap(); - let workspace_root = AbsolutePathBuf::new(temp_dir.path().join("nonexistent")).unwrap(); - - let command = InstallCommandBuilder::new(workspace_root).build(); - let args = vec![]; - - let result = command.execute(&args).await; - let err = result.unwrap_err(); - assert!(matches!(err, Error::WorkspaceError(_))); - } - - /// Test that in CI environment, we will use pnpm without prompting - #[test] - #[serial] // Run serially to avoid race conditions with environment variables - fn test_prompt_package_manager_in_ci() { - // Use EnvGuard to safely manage the CI environment variable - let _guard = EnvGuard::new("CI", "true"); - - // Should return pnpm without prompting - let result = prompt_package_manager_selection(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), PackageManagerType::Pnpm); - - // EnvGuard will automatically restore the original value when dropped - } -} diff --git a/packages/cli/binding/src/commands/lib_cmd.rs b/packages/cli/binding/src/commands/lib_cmd.rs deleted file mode 100644 index f24045a6..00000000 --- a/packages/cli/binding/src/commands/lib_cmd.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::future::Future; - -use petgraph::stable_graph::StableGraph; -use vite_error::Error as ViteError; -use vite_task::{ - Error, ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace, -}; - -#[tracing::instrument(skip(resolve_lib_command, workspace))] -pub async fn lib< - Lib: Future>, - LibFn: Fn() -> Lib, ->( - resolve_lib_command: LibFn, - workspace: &Workspace, - args: &Vec, -) -> Result { - let wrapped_command = - || async { resolve_lib_command().await.map_err(|e| Error::Anyhow(e.into())) }; - let resolved_task = - ResolvedTask::resolve_from_builtin(workspace, wrapped_command, "lib", args.iter()).await?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - ExecutionPlan::plan(task_graph, false)?.execute(workspace).await -} diff --git a/packages/cli/binding/src/commands/link.rs b/packages/cli/binding/src/commands/link.rs deleted file mode 100644 index 374af777..00000000 --- a/packages/cli/binding/src/commands/link.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{commands::link::LinkCommandOptions, package_manager::PackageManager}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Link command for local package development. -/// -/// This command automatically detects the package manager and translates -/// the link command to the appropriate package manager-specific syntax. -pub struct LinkCommand { - cwd: AbsolutePathBuf, -} - -impl LinkCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - package: Option<&str>, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let link_command_options = LinkCommandOptions { package, pass_through_args }; - package_manager.run_link_command(&link_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_link_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = LinkCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/lint.rs b/packages/cli/binding/src/commands/lint.rs deleted file mode 100644 index 4fca5289..00000000 --- a/packages/cli/binding/src/commands/lint.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::{collections::HashMap, future::Future}; - -use petgraph::stable_graph::StableGraph; -use serde::{Deserialize, Serialize}; -use vite_error::Error as ViteError; -use vite_task::{ - Error, ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LintConfig { - pub rules: HashMap, -} - -#[tracing::instrument(skip(resolve_lint_command, workspace))] -pub async fn lint< - Lint: Future>, - LintFn: Fn() -> Lint, ->( - resolve_lint_command: LintFn, - workspace: &Workspace, - args: &Vec, -) -> Result { - let wrapped_command = - || async { resolve_lint_command().await.map_err(|e| Error::Anyhow(e.into())) }; - let resolved_task = - ResolvedTask::resolve_from_builtin(workspace, wrapped_command, "lint", args.iter()).await?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - ExecutionPlan::plan(task_graph, false)?.execute(workspace).await -} diff --git a/packages/cli/binding/src/commands/mod.rs b/packages/cli/binding/src/commands/mod.rs deleted file mode 100644 index 0164630f..00000000 --- a/packages/cli/binding/src/commands/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub(crate) mod add; -pub(crate) mod dedupe; -pub(crate) mod doc; -pub(crate) mod fmt; -pub(crate) mod install; -pub(crate) mod lib_cmd; -pub(crate) mod link; -pub(crate) mod lint; -pub(crate) mod outdated; -pub(crate) mod remove; -pub(crate) mod test; -pub(crate) mod unlink; -pub(crate) mod update; -pub(crate) mod vite; -pub(crate) mod why; diff --git a/packages/cli/binding/src/commands/outdated.rs b/packages/cli/binding/src/commands/outdated.rs deleted file mode 100644 index e159b58e..00000000 --- a/packages/cli/binding/src/commands/outdated.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{ - commands::outdated::{Format, OutdatedCommandOptions}, - package_manager::PackageManager, -}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Outdated command for checking outdated packages. -/// -/// This command automatically detects the package manager and translates -/// the outdated command to the appropriate package manager-specific syntax. -pub struct OutdatedCommand { - cwd: AbsolutePathBuf, -} - -impl OutdatedCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - #[allow(clippy::too_many_arguments)] - pub async fn execute( - self, - packages: &[String], - long: bool, - format: Option, - recursive: bool, - filters: Option<&[String]>, - workspace_root: bool, - prod: bool, - dev: bool, - no_optional: bool, - compatible: bool, - sort_by: Option<&str>, - global: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let outdated_command_options = OutdatedCommandOptions { - packages, - long, - format, - recursive, - filters, - workspace_root, - prod, - dev, - no_optional, - compatible, - sort_by, - global, - pass_through_args, - }; - package_manager.run_outdated_command(&outdated_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_outdated_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = OutdatedCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/remove.rs b/packages/cli/binding/src/commands/remove.rs deleted file mode 100644 index 2ba07986..00000000 --- a/packages/cli/binding/src/commands/remove.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{commands::remove::RemoveCommandOptions, package_manager::PackageManager}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Remove command for removing packages from dependencies. -/// -/// This command automatically detects the package manager and translates -/// the remove command to the appropriate package manager-specific syntax. -pub struct RemoveCommand { - cwd: AbsolutePathBuf, -} - -impl RemoveCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - packages: &[String], - save_dev: bool, - save_optional: bool, - save_prod: bool, - filters: Option<&[String]>, - workspace_root: bool, - recursive: bool, - global: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let remove_command_options = RemoveCommandOptions { - packages, - filters, - workspace_root, - recursive, - global, - save_dev, - save_optional, - save_prod, - pass_through_args, - }; - package_manager.run_remove_command(&remove_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_remove_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = RemoveCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/test.rs b/packages/cli/binding/src/commands/test.rs deleted file mode 100644 index 6ee045ec..00000000 --- a/packages/cli/binding/src/commands/test.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::future::Future; - -use petgraph::stable_graph::StableGraph; -use vite_error::Error as ViteError; -use vite_task::{ - Error, ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace, -}; - -pub async fn test< - Test: Future>, - TestFn: Fn() -> Test, ->( - resolve_test_command: TestFn, - workspace: &Workspace, - args: &Vec, -) -> Result { - let wrapped_command = - || async { resolve_test_command().await.map_err(|e| Error::Anyhow(e.into())) }; - let resolved_task = ResolvedTask::resolve_from_builtin( - workspace, - wrapped_command, - "test", - args.iter().map(std::string::String::as_str), - ) - .await?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - ExecutionPlan::plan(task_graph, false)?.execute(workspace).await -} diff --git a/packages/cli/binding/src/commands/unlink.rs b/packages/cli/binding/src/commands/unlink.rs deleted file mode 100644 index a3e81a69..00000000 --- a/packages/cli/binding/src/commands/unlink.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{commands::unlink::UnlinkCommandOptions, package_manager::PackageManager}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Unlink command for removing package links. -/// -/// This command automatically detects the package manager and translates -/// the unlink command to the appropriate package manager-specific syntax. -pub struct UnlinkCommand { - cwd: AbsolutePathBuf, -} - -impl UnlinkCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - package: Option<&str>, - recursive: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let unlink_command_options = UnlinkCommandOptions { package, recursive, pass_through_args }; - package_manager.run_unlink_command(&unlink_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_unlink_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = UnlinkCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/update.rs b/packages/cli/binding/src/commands/update.rs deleted file mode 100644 index 9da609a3..00000000 --- a/packages/cli/binding/src/commands/update.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{commands::update::UpdateCommandOptions, package_manager::PackageManager}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Update command for updating packages to their latest versions. -/// -/// This command automatically detects the package manager and translates -/// the update command to the appropriate package manager-specific syntax. -pub struct UpdateCommand { - cwd: AbsolutePathBuf, -} - -impl UpdateCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - packages: &[String], - latest: bool, - global: bool, - recursive: bool, - filters: Option<&[String]>, - workspace_root: bool, - dev: bool, - prod: bool, - interactive: bool, - no_optional: bool, - no_save: bool, - workspace_only: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let update_command_options = UpdateCommandOptions { - packages, - latest, - global, - recursive, - filters, - workspace_root, - dev, - prod, - interactive, - no_optional, - no_save, - workspace_only, - pass_through_args, - }; - package_manager.run_update_command(&update_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_update_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = UpdateCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/commands/vite.rs b/packages/cli/binding/src/commands/vite.rs deleted file mode 100644 index cf39a55d..00000000 --- a/packages/cli/binding/src/commands/vite.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::{future::Future, iter}; - -use petgraph::stable_graph::StableGraph; -use vite_error::Error as ViteError; -use vite_task::{ - Error, ExecutionPlan, ExecutionSummary, ResolveCommandResult, ResolvedTask, Workspace, -}; - -pub async fn vite< - Vite: Future>, - ViteFn: Fn() -> Vite, ->( - arg_forward: &str, - resolve_vite_command: ViteFn, - workspace: &Workspace, - args: &Vec, -) -> Result { - let wrapped_command = - || async { resolve_vite_command().await.map_err(|e| Error::Anyhow(e.into())) }; - let resolved_task = ResolvedTask::resolve_from_builtin( - workspace, - wrapped_command, - arg_forward, - iter::once(arg_forward).chain(args.iter().map(std::string::String::as_str)), - ) - .await?; - let mut task_graph: StableGraph = Default::default(); - task_graph.add_node(resolved_task); - ExecutionPlan::plan(task_graph, false)?.execute(workspace).await -} diff --git a/packages/cli/binding/src/commands/why.rs b/packages/cli/binding/src/commands/why.rs deleted file mode 100644 index 6e289f4f..00000000 --- a/packages/cli/binding/src/commands/why.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{commands::why::WhyCommandOptions, package_manager::PackageManager}; -use vite_path::AbsolutePathBuf; - -use crate::Error; - -/// Why command for showing why a package is installed. -/// -/// This command automatically detects the package manager and translates -/// the why command to the appropriate package manager-specific syntax. -pub struct WhyCommand { - cwd: AbsolutePathBuf, -} - -impl WhyCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - #[allow(clippy::too_many_arguments)] - pub async fn execute( - self, - packages: &[String], - json: bool, - long: bool, - parseable: bool, - recursive: bool, - filters: Option<&[String]>, - workspace_root: bool, - prod: bool, - dev: bool, - depth: Option, - no_optional: bool, - global: bool, - exclude_peers: bool, - find_by: Option<&str>, - pass_through_args: Option<&[String]>, - ) -> Result { - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build().await?; - - let why_command_options = WhyCommandOptions { - packages, - json, - long, - parseable, - recursive, - filters, - workspace_root, - prod, - dev, - depth, - no_optional, - global, - exclude_peers, - find_by, - pass_through_args, - }; - package_manager.run_why_command(&why_command_options, &self.cwd).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_why_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = WhyCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs deleted file mode 100644 index 166b2c82..00000000 --- a/packages/cli/binding/src/lib.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! NAPI binding layer for vite-plus CLI -//! -//! This module provides the bridge between JavaScript tool resolvers and the Rust core. -//! It uses NAPI-RS to create native Node.js bindings that allow JavaScript functions -//! to be called from Rust code. -//! -//! ## Architecture -//! -//! The binding follows a callback pattern: -//! 1. JavaScript passes resolver functions to Rust through `CliOptions` -//! 2. These functions are wrapped in `ThreadsafeFunction` for safe cross-runtime calls -//! 3. When Rust needs a tool, it calls the corresponding JavaScript function -//! 4. JavaScript resolves the tool path and returns it to Rust -//! 5. Rust executes the tool with the resolved path - -mod cli; -mod commands; - -use std::{collections::HashMap, sync::Arc}; - -use clap::Parser as _; -use napi::{anyhow, bindgen_prelude::*, threadsafe_function::ThreadsafeFunction}; -use napi_derive::napi; -use vite_error::Error; -use vite_path::current_dir; -use vite_task::ResolveCommandResult; - -use crate::cli::{Args, CliOptions as ViteTaskCliOptions, Commands}; - -/// Module initialization - sets up tracing for debugging -#[napi_derive::module_init] -pub fn init() { - crate::cli::init_tracing(); -} - -/// Configuration options passed from JavaScript to Rust. -/// -/// Each field (except `cwd`) is a JavaScript function wrapped in a `ThreadsafeFunction`. -/// These functions are called by Rust to resolve tool binary paths when needed. -/// -/// The `ThreadsafeFunction` wrapper ensures the JavaScript functions can be -/// safely called from Rust's async runtime without blocking or race conditions. -#[napi(object, object_to_js = false)] -pub struct CliOptions { - /// Resolver function for the lint tool (oxlint) - pub lint: Arc>>, - /// Resolver function for the fmt tool (oxfmt) - pub fmt: Arc>>, - /// Resolver function for the vite tool (used for build/dev) - pub vite: Arc>>, - /// Resolver function for the test tool (vitest) - pub test: Arc>>, - /// Resolver function for the lib tool (tsdown) - pub lib: Arc>>, - /// Resolver function for the doc tool (vitepress) - pub doc: Arc>>, - /// Optional working directory override - pub cwd: Option, - /// Read the vite.config.ts in the Node.js side and return the `lint` and `fmt` config JSON string back to the Rust side - pub resolve_universal_vite_config: Arc>>, -} - -/// Result returned by JavaScript resolver functions. -/// -/// This structure contains the information needed to execute a tool: -/// - `bin_path`: The absolute path to the tool's binary/script -/// - `envs`: Environment variables to set when executing the tool -#[napi(object, object_to_js = false)] -pub struct JsCommandResolvedResult { - /// Absolute path to the tool's executable or script - pub bin_path: String, - /// Environment variables to set when running the tool - pub envs: HashMap, -} - -/// Convert JavaScript result to Rust's expected format -impl From for ResolveCommandResult { - fn from(value: JsCommandResolvedResult) -> Self { - Self { bin_path: value.bin_path, envs: value.envs } - } -} - -static BUILTIN_COMMANDS: &[&str] = &["lint", "fmt", "build", "test", "doc", "lib"]; - -/// Main entry point for the CLI, called from JavaScript. -/// -/// This function: -/// 1. Parses command-line arguments -/// 2. Sets up the working directory -/// 3. Creates Rust-callable wrappers for JavaScript resolver functions -/// 4. Passes control to the Rust core (`vite_task::main`) -/// -/// ## JavaScript-to-Rust Bridge -/// -/// The resolver functions are wrapped to: -/// - Call the JavaScript function asynchronously -/// - Handle errors and convert them to Rust error types -/// - Convert the JavaScript result to Rust's expected format -/// -/// ## Error Handling -/// -/// Errors from JavaScript resolvers are converted to specific error types -/// (e.g., `LintFailed`, `ViteError`) to provide better error messages. -#[napi] -pub async fn run(options: CliOptions) -> Result { - let args = parse_args(); - // Use provided cwd or current directory - let mut cwd = current_dir()?; - if let Some(options_cwd) = options.cwd { - cwd.push(options_cwd); - } - // Extract resolver functions from options - let lint = options.lint; - let fmt = options.fmt; - let vite = options.vite; - let test = options.test; - let lib = options.lib; - let doc = options.doc; - let resolve_universal_vite_config = options.resolve_universal_vite_config; - // Call the Rust core with wrapped resolver functions - let result = crate::cli::main( - cwd, - args, - Some(ViteTaskCliOptions { - // Wrap the lint resolver to be callable from Rust - lint: || async { - // Call the JavaScript function and await both the promise and the result - let resolved = lint - .call_async(Ok(())) // Call with no arguments - .await // Wait for the call to complete - .map_err(js_error_to_lint_error)? // Convert call errors - .await // Wait for the promise to resolve - .map_err(js_error_to_lint_error)?; // Convert promise errors - - Ok(resolved.into()) // Convert to Rust type - }, - // Wrap the fmt resolver to be callable from Rust - fmt: || async { - let resolved = fmt - .call_async(Ok(())) - .await - .map_err(js_error_to_fmt_error)? - .await - .map_err(js_error_to_fmt_error)?; - - Ok(resolved.into()) - }, - // Wrap the vite resolver to be callable from Rust - vite: || async { - let resolved = vite - .call_async(Ok(())) - .await - .map_err(js_error_to_vite_error)? - .await - .map_err(js_error_to_vite_error)?; - - Ok(resolved.into()) - }, - // Wrap the test resolver to be callable from Rust - test: || async { - let resolved = test - .call_async(Ok(())) - .await - .map_err(js_error_to_test_error)? - .await - .map_err(js_error_to_test_error)?; - - Ok(resolved.into()) - }, - // Wrap the lib resolver to be callable from Rust - lib: || async { - let resolved = lib - .call_async(Ok(())) - .await - .map_err(js_error_to_lib_error)? - .await - .map_err(js_error_to_lib_error)?; - - Ok(resolved.into()) - }, - // Wrap the doc resolver to be callable from Rust - doc: || async { - let resolved = doc - .call_async(Ok(())) - .await - .map_err(js_error_to_doc_error)? - .await - .map_err(js_error_to_doc_error)?; - - Ok(resolved.into()) - }, - resolve_universal_vite_config: |cwd: String| async { - let resolved = resolve_universal_vite_config - .call_async(Ok(cwd)) - .await - .map_err(js_error_to_resolve_universal_vite_config_error)? - .await - .map_err(js_error_to_resolve_universal_vite_config_error)?; - Ok(resolved) - }, - }), - ) - .await; - - tracing::debug!("Result: {result:?}"); - - match result { - Ok(exit_status) => Ok(exit_status.code().unwrap_or(1)), - Err(e) => { - match e { - // Standard exit code for Ctrl+C - Error::UserCancelled => Ok(130), - _ => { - // Convert Rust errors to NAPI errors for JavaScript - tracing::error!("Rust error: {:?}", e); - Err(anyhow::Error::from(e).into()) - } - } - } - } -} - -/// Convert JavaScript errors to Rust lint errors -fn js_error_to_lint_error(err: napi::Error) -> Error { - Error::LintFailed { status: err.status.to_string().into(), reason: err.to_string().into() } -} - -/// Convert JavaScript errors to Rust fmt errors -fn js_error_to_fmt_error(err: napi::Error) -> Error { - Error::FmtFailed { status: err.status.to_string().into(), reason: err.to_string().into() } -} - -/// Convert JavaScript errors to Rust vite errors -fn js_error_to_vite_error(err: napi::Error) -> Error { - Error::Vite { status: err.status.to_string().into(), reason: err.to_string().into() } -} - -/// Convert JavaScript errors to Rust test errors -fn js_error_to_test_error(err: napi::Error) -> Error { - Error::TestFailed { status: err.status.to_string().into(), reason: err.to_string().into() } -} - -/// Convert JavaScript errors to Rust lib errors -fn js_error_to_lib_error(err: napi::Error) -> Error { - Error::LibFailed { status: err.status.to_string().into(), reason: err.to_string().into() } -} - -/// Convert JavaScript errors to Rust doc errors -fn js_error_to_doc_error(err: napi::Error) -> Error { - Error::DocFailed { status: err.status.to_string().into(), reason: err.to_string().into() } -} - -/// Convert JavaScript errors to Rust resolve universal vite config errors -fn js_error_to_resolve_universal_vite_config_error(err: napi::Error) -> Error { - Error::ResolveUniversalViteConfigFailed { - status: err.status.to_string().into(), - reason: err.to_string().into(), - } -} - -fn parse_args() -> Args { - // ArgsOs [node, vite-plus, ...] - let mut raw_args = std::env::args_os().skip(2); - if let Some(first) = raw_args.next() - && let Some(first) = first.to_str() - && BUILTIN_COMMANDS.contains(&first) - { - let forwarded_args = raw_args - .map(|a| a.into_string().unwrap_or_else(|os_str| os_str.to_string_lossy().into_owned())) - .collect(); - return Args { - task: None, - task_args: vec![], - commands: match first { - "lint" => Commands::Lint { args: forwarded_args }, - "fmt" => Commands::Fmt { args: forwarded_args }, - "build" => Commands::Build { args: forwarded_args }, - "test" => Commands::Test { args: forwarded_args }, - "doc" => Commands::Doc { args: forwarded_args }, - "lib" => Commands::Lib { args: forwarded_args }, - _ => unreachable!(), - }, - debug: false, - no_debug: true, - }; - } - // Parse CLI arguments (skip first arg which is the node binary) - Args::parse_from(std::env::args_os().skip(1)) -} diff --git a/packages/cli/binding/src/main.rs b/packages/cli/binding/src/main.rs deleted file mode 100644 index 4b50389c..00000000 --- a/packages/cli/binding/src/main.rs +++ /dev/null @@ -1,31 +0,0 @@ -use clap::Parser as _; -use vite_error::Error; -use vite_path::current_dir; - -mod cli; -mod commands; - -use cli::{Args, CliOptions, init_tracing}; - -#[tokio::main] - -async fn main() -> Result<(), Error> { - init_tracing(); - - let args = Args::parse(); - - let result = cli::main(current_dir()?, args, None::).await; - - match result { - Ok(exit_status) => std::process::exit(exit_status.code().unwrap_or(1)), - - Err(err) => { - tracing::error!("Error: {}", err); - match err { - // Standard exit code for Ctrl+C - Error::UserCancelled => std::process::exit(130), - _ => return Err(err), - } - } - } -} diff --git a/packages/cli/build.ts b/packages/cli/build.ts deleted file mode 100644 index dd2ab98c..00000000 --- a/packages/cli/build.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { copyFile } from 'node:fs/promises'; -import { join, parse } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { parseArgs } from 'node:util'; - -import { NapiCli } from '@napi-rs/cli'; -import { build } from 'rolldown'; -import { - createCompilerHost, - createProgram, - formatDiagnostics, - parseJsonSourceFileConfigFileContent, - readJsonConfigFile, - sys, -} from 'typescript'; - -const { values: { target, x, [`use-napi-cross`]: useNapiCross } } = parseArgs({ - options: { - target: { - type: 'string', - }, - x: { - type: 'boolean', - default: false, - }, - [`use-napi-cross`]: { - type: 'boolean', - default: false, - }, - }, -}); - -const cli = new NapiCli(); -const { task } = await cli.build({ - packageJsonPath: '../package.json', - cwd: 'binding', - platform: true, - release: process.env.VITE_PLUS_CLI_DEBUG !== '1', - esm: true, - target, - crossCompile: x, - useNapiCross, -}); - -const output = (await task).find((o) => o.kind === 'node'); - -await build({ - input: ['./src/bin.ts', './src/index.ts', './src/test.ts'], - external: [/^node:/, 'rolldown-vite', 'vitest'], - output: { - format: 'esm', - }, -}); - -if (output) { - await copyFile(output.path, `./dist/${parse(output.path).base}`); -} - -const projectDir = join(fileURLToPath(import.meta.url), '..'); - -const tsconfig = readJsonConfigFile(join(projectDir, 'tsconfig.json'), sys.readFile); - -const { options } = parseJsonSourceFileConfigFileContent(tsconfig, sys, projectDir); - -const host = createCompilerHost(options); - -const srcDir = join(projectDir, 'src'); -const program = createProgram({ - rootNames: [ - join(srcDir, 'index.ts'), - join(srcDir, 'client.ts'), - join(srcDir, 'test.ts'), - ], - options: { - ...options, - emitDeclarationOnly: true, - }, - host, -}); - -const { diagnostics } = program.emit(); - -if (diagnostics.length > 0) { - console.error(formatDiagnostics(diagnostics, host)); - process.exit(1); -} diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index 5cf6b9cc..00000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "@voidzero-dev/vite-plus", - "version": "0.0.0", - "type": "module", - "bin": { - "vite": "./bin/vite", - "vite-plus": "./bin/vite" - }, - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./bin": { - "import": "./dist/bin.js" - }, - "./client": { - "types": "./dist/client.d.ts" - }, - "./test": { - "import": "./dist/test.js", - "types": "./dist/test.d.ts" - } - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "scripts": { - "build": "oxnode ./build.ts", - "snap-test": "tool snap-test" - }, - "files": [ - "bin", - "dist" - ], - "dependencies": { - "oxfmt": "catalog:", - "oxlint": "catalog:", - "oxlint-tsgolint": "catalog:", - "rolldown-vite": "catalog:", - "tsdown": "catalog:", - "vitepress": "catalog:", - "vitest": "catalog:" - }, - "devDependencies": { - "@napi-rs/cli": "catalog:", - "@oxc-node/core": "catalog:", - "@vitest/browser": "catalog:", - "@vitest/browser-playwright": "catalog:", - "@voidzero-dev/vite-plus-tools": "workspace:", - "playwright": "catalog:", - "rolldown": "catalog:" - }, - "napi": { - "binaryName": "vite-plus", - "packageName": "@vite-plus", - "targets": [ - "aarch64-apple-darwin", - "x86_64-apple-darwin", - "aarch64-unknown-linux-gnu", - "x86_64-unknown-linux-gnu", - "aarch64-pc-windows-msvc", - "x86_64-pc-windows-msvc" - ] - } -} diff --git a/packages/cli/snap-tests-todo/pnpm-install-with-options/package.json b/packages/cli/snap-tests-todo/pnpm-install-with-options/package.json deleted file mode 100644 index e9bec0fc..00000000 --- a/packages/cli/snap-tests-todo/pnpm-install-with-options/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "pnpm-install-with-options", - "version": "1.0.0", - "private": true, - "packageManager": "pnpm@10.15.1", - "dependencies": { - "tslib": "2.8.1" - }, - "devDependencies": { - "oxlint": "latest" - } -} diff --git a/packages/cli/snap-tests-todo/pnpm-install-with-options/snap.txt b/packages/cli/snap-tests-todo/pnpm-install-with-options/snap.txt deleted file mode 100644 index a29ef62c..00000000 --- a/packages/cli/snap-tests-todo/pnpm-install-with-options/snap.txt +++ /dev/null @@ -1,50 +0,0 @@ -> vite install --help | head -n 20 # print help message -Version (compiled to binary; bundled Node.js v) -Usage: pnpm install [options] - -Alias: i - -Installs all dependencies of the project in the current working directory. When executed inside a workspace, installs all dependencies of all projects. - -Options: - --[no-]color Controls colors in the output. By - default, output is always colored when - it goes directly to a terminal - --[no-]frozen-lockfile Don't generate a lockfile and fail if an - update is needed. This setting is on by - default in CI environments, so use - --no-frozen-lockfile if you need to - disable it for some reason - --[no-]verify-store-integrity If false, doesn't check whether packages - in the store were mutated - --aggregate-output Aggregate output from child processes - -> vite install --prod # https://pnpm.io/cli/install#--prod--p -Packages: + -+ -Progress: resolved , reused , downloaded , added , done - -dependencies: -+ tslib - -devDependencies: skipped - -Done in ms using pnpm v - - -> ls node_modules -tslib - -> vite install --prod # install again hit cache -✓ cache hit, replaying -Packages: + -+ -Progress: resolved , reused , downloaded , added , done - -dependencies: -+ tslib - -devDependencies: skipped - -Done in ms using pnpm v - diff --git a/packages/cli/snap-tests-todo/pnpm-install-with-options/steps.json b/packages/cli/snap-tests-todo/pnpm-install-with-options/steps.json deleted file mode 100644 index 0e99f3f8..00000000 --- a/packages/cli/snap-tests-todo/pnpm-install-with-options/steps.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite install --help | head -n 20 # print help message", - "vite install --prod # https://pnpm.io/cli/install#--prod--p", - "ls node_modules", - "vite install --prod # install again hit cache" - ] -} diff --git a/packages/cli/snap-tests-todo/test-panicked-fix/package.json b/packages/cli/snap-tests-todo/test-panicked-fix/package.json deleted file mode 100644 index 40377097..00000000 --- a/packages/cli/snap-tests-todo/test-panicked-fix/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "test-panicked-fix", - "version": "1.0.0", - "private": true, - "packageManager": "pnpm@10.17.0" -} diff --git a/packages/cli/snap-tests-todo/test-panicked-fix/snap.txt b/packages/cli/snap-tests-todo/test-panicked-fix/snap.txt deleted file mode 100644 index 85c79aeb..00000000 --- a/packages/cli/snap-tests-todo/test-panicked-fix/snap.txt +++ /dev/null @@ -1,27 +0,0 @@ -> vite lint --help | head -n 20 # print help message and no panicked -Usage: [-c=<./.oxlintrc.json>] [PATH]... - -Basic Configuration - -c, --config=<./.oxlintrc.json> Oxlint configuration file (experimental) - * only `.json` extension is supported - * you can use comments in configuration files. - * tries to be compatible with the ESLint v8's format - --tsconfig=<./tsconfig.json> TypeScript `tsconfig.json` path for reading path alias and - project references for import plugin - --init Initialize oxlint configuration with default values - -Allowing / Denying Multiple Lints - Accumulate rules and categories from left to right on the command-line. - For example `-D correctness -A no-debugger` or `-A all -D no-debugger`. - The categories are: - * `correctness` - code that is outright wrong or useless (default). - * `suspicious` - code that is most likely wrong or useless. - * `pedantic` - lints which are rather strict or have occasional false positives. - * `style` - code that should be written in a more idiomatic way. - * `nursery` - new lints that are still under development. - - -thread 'tokio-runtime-worker' panicked at library/std/src/io/stdio.rs:1165:9: -failed printing to stdout: Broken pipe (os error 32) -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -[vite+] run error: [Error: Panic in async function] { code: 'GenericFailure' } diff --git a/packages/cli/snap-tests-todo/test-panicked-fix/steps.json b/packages/cli/snap-tests-todo/test-panicked-fix/steps.json deleted file mode 100644 index 81d84505..00000000 --- a/packages/cli/snap-tests-todo/test-panicked-fix/steps.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite lint --help | head -n 20 # print help message and no panicked" - ] -} diff --git a/packages/cli/snap-tests/associate-existing-cache/package.json b/packages/cli/snap-tests/associate-existing-cache/package.json deleted file mode 100644 index 05386b90..00000000 --- a/packages/cli/snap-tests/associate-existing-cache/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "scripts": { - "script1": "echo hello", - "script2": "echo hello" - } -} diff --git a/packages/cli/snap-tests/associate-existing-cache/snap.txt b/packages/cli/snap-tests/associate-existing-cache/snap.txt deleted file mode 100644 index 052b6566..00000000 --- a/packages/cli/snap-tests/associate-existing-cache/snap.txt +++ /dev/null @@ -1,54 +0,0 @@ -> vite run script1 # create the cache for 'echo hello' -$ echo hello -hello - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] script1: $ echo hello ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> vite run script2 # script2 has the same command as script1, should hit and associate the cache -$ echo hello (✓ cache hit, replaying) -hello - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 1 cache hits • 0 cache misses -Performance: 100% cache hit rate, ms saved in total - -Task Details: -──────────────────────────────────────────────── - [1] script2: $ echo hello ✓ - → Cache hit - output replayed - ms saved -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> json-edit package.json '_.scripts.script2 = "echo world"' # update the command of script2 -> vite run script2 # should report cache miss due to command mismatch with the cache associated in step 2 -$ echo world (✗ cache miss: command changed, executing) -world - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] script2: $ echo world ✓ - → Cache miss: command changed from echo hello to echo world -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/packages/cli/snap-tests/associate-existing-cache/steps.json b/packages/cli/snap-tests/associate-existing-cache/steps.json deleted file mode 100644 index 0e960d32..00000000 --- a/packages/cli/snap-tests/associate-existing-cache/steps.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite run script1 # create the cache for 'echo hello'", - "vite run script2 # script2 has the same command as script1, should hit and associate the cache", - "json-edit package.json '_.scripts.script2 = \"echo world\"' # update the command of script2", - "vite run script2 # should report cache miss due to command mismatch with the cache associated in step 2" - ] -} diff --git a/packages/cli/snap-tests/builtin-different-cwd/folder1/a.js b/packages/cli/snap-tests/builtin-different-cwd/folder1/a.js deleted file mode 100644 index 018d086c..00000000 --- a/packages/cli/snap-tests/builtin-different-cwd/folder1/a.js +++ /dev/null @@ -1 +0,0 @@ -console.log('folder1'); diff --git a/packages/cli/snap-tests/builtin-different-cwd/folder2/a.js b/packages/cli/snap-tests/builtin-different-cwd/folder2/a.js deleted file mode 100644 index 118be0bb..00000000 --- a/packages/cli/snap-tests/builtin-different-cwd/folder2/a.js +++ /dev/null @@ -1 +0,0 @@ -console.log('folder2'); diff --git a/packages/cli/snap-tests/builtin-different-cwd/package.json b/packages/cli/snap-tests/builtin-different-cwd/package.json deleted file mode 100644 index 0967ef42..00000000 --- a/packages/cli/snap-tests/builtin-different-cwd/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/packages/cli/snap-tests/builtin-different-cwd/snap.txt b/packages/cli/snap-tests/builtin-different-cwd/snap.txt deleted file mode 100644 index 60b585a5..00000000 --- a/packages/cli/snap-tests/builtin-different-cwd/snap.txt +++ /dev/null @@ -1,22 +0,0 @@ -> cd folder1 && vite lint -Found 0 warnings and 0 errors. -Finished in ms on 1 file with rules using threads. - - -> cd folder2 && vite lint # should create different cache even though the package is the same -Found 0 warnings and 0 errors. -Finished in ms on 1 file with rules using threads. - - -> echo 'console.log(1);' > folder2/a.js -> cd folder1 && vite lint -✓ cache hit, replaying -Found 0 warnings and 0 errors. -Finished in ms on 1 file with rules using threads. - - -> cd folder2 && vite lint -✗ cache miss: content of input 'folder2/a.js' changed, executing -Found 0 warnings and 0 errors. -Finished in ms on 1 file with rules using threads. - diff --git a/packages/cli/snap-tests/builtin-different-cwd/steps.json b/packages/cli/snap-tests/builtin-different-cwd/steps.json deleted file mode 100644 index d12915b1..00000000 --- a/packages/cli/snap-tests/builtin-different-cwd/steps.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "cd folder1 && vite lint", - "cd folder2 && vite lint # should create different cache even though the package is the same", - "echo 'console.log(1);' > folder2/a.js", - "cd folder1 && vite lint", - "cd folder2 && vite lint" - ] -} diff --git a/packages/cli/snap-tests/cache-clean/package.json b/packages/cli/snap-tests/cache-clean/package.json deleted file mode 100644 index 27676367..00000000 --- a/packages/cli/snap-tests/cache-clean/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "scripts": { - "hello": "echo hello" - } -} diff --git a/packages/cli/snap-tests/cache-clean/snap.txt b/packages/cli/snap-tests/cache-clean/snap.txt deleted file mode 100644 index 6a1332c0..00000000 --- a/packages/cli/snap-tests/cache-clean/snap.txt +++ /dev/null @@ -1,73 +0,0 @@ -> vite run hello # create the cache for 'echo hello' -$ echo hello -hello - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ echo hello ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> vite run hello # hit the cache -$ echo hello (✓ cache hit, replaying) -hello - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 1 cache hits • 0 cache misses -Performance: 100% cache hit rate, ms saved in total - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ echo hello ✓ - → Cache hit - output replayed - ms saved -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> vite cache clean # clean the cache -> vite run hello # cache miss after clean -$ echo hello -hello - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ echo hello ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> cd subfolder && vite cache clean # cache can be located and cleaned from subfolder -> vite run hello # cache miss after clean -$ echo hello -hello - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ echo hello ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/packages/cli/snap-tests/cache-clean/steps.json b/packages/cli/snap-tests/cache-clean/steps.json deleted file mode 100644 index fb7f548c..00000000 --- a/packages/cli/snap-tests/cache-clean/steps.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite run hello # create the cache for 'echo hello'", - "vite run hello # hit the cache", - "vite cache clean # clean the cache", - "vite run hello # cache miss after clean", - "cd subfolder && vite cache clean # cache can be located and cleaned from subfolder", - "vite run hello # cache miss after clean" - ] -} diff --git a/packages/cli/snap-tests/cache-clean/subfolder/.gitkeep b/packages/cli/snap-tests/cache-clean/subfolder/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/cli/snap-tests/cache-miss-command-change/package.json b/packages/cli/snap-tests/cache-miss-command-change/package.json deleted file mode 100644 index f155bcac..00000000 --- a/packages/cli/snap-tests/cache-miss-command-change/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "scripts": { - "hello": "echo foo && echo bar" - } -} diff --git a/packages/cli/snap-tests/cache-miss-command-change/snap.txt b/packages/cli/snap-tests/cache-miss-command-change/snap.txt deleted file mode 100644 index cefa9ddc..00000000 --- a/packages/cli/snap-tests/cache-miss-command-change/snap.txt +++ /dev/null @@ -1,67 +0,0 @@ -> vite run hello # create caches for 'echo foo' and 'echo bar' -$ echo foo -foo - -$ echo bar -bar - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 2 tasks • 0 cache hits • 2 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] hello(subcommand 0): $ echo foo ✓ - → Cache miss: no previous cache entry found - ······················································· - [2] hello: $ echo bar ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> json-edit package.json '_.scripts.hello = "echo baz && echo bar"' # update the first subcommand -> vite run hello # should report cache miss of the first subcommand, and hit cache of the second subcommand -$ echo baz (✗ cache miss: command changed, executing) -baz - -$ echo bar (✓ cache hit, replaying) -bar - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 2 tasks • 1 cache hits • 1 cache misses -Performance: 50% cache hit rate, ms saved in total - -Task Details: -──────────────────────────────────────────────── - [1] hello(subcommand 0): $ echo baz ✓ - → Cache miss: command changed from echo foo to echo baz - ······················································· - [2] hello: $ echo bar ✓ - → Cache hit - output replayed - ms saved -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> json-edit package.json '_.scripts.hello = "echo bar"' # remove the first subcommand, now the second subcommand becomes the first -> vite run hello # can still hit cache even the subcommand index changed -$ echo bar (✓ cache hit, replaying) -bar - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 1 cache hits • 0 cache misses -Performance: 100% cache hit rate, ms saved in total - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ echo bar ✓ - → Cache hit - output replayed - ms saved -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/packages/cli/snap-tests/cache-miss-command-change/steps.json b/packages/cli/snap-tests/cache-miss-command-change/steps.json deleted file mode 100644 index 854781ee..00000000 --- a/packages/cli/snap-tests/cache-miss-command-change/steps.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite run hello # create caches for 'echo foo' and 'echo bar'", - "json-edit package.json '_.scripts.hello = \"echo baz && echo bar\"' # update the first subcommand", - "vite run hello # should report cache miss of the first subcommand, and hit cache of the second subcommand", - "json-edit package.json '_.scripts.hello = \"echo bar\"' # remove the first subcommand, now the second subcommand becomes the first", - "vite run hello # can still hit cache even the subcommand index changed" - ] -} diff --git a/packages/cli/snap-tests/change-passthrough-env-config/package.json b/packages/cli/snap-tests/change-passthrough-env-config/package.json deleted file mode 100644 index 0967ef42..00000000 --- a/packages/cli/snap-tests/change-passthrough-env-config/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/packages/cli/snap-tests/change-passthrough-env-config/snap.txt b/packages/cli/snap-tests/change-passthrough-env-config/snap.txt deleted file mode 100644 index d29b6fb9..00000000 --- a/packages/cli/snap-tests/change-passthrough-env-config/snap.txt +++ /dev/null @@ -1,54 +0,0 @@ -> MY_ENV=1 vite run hello -$ node -p process.env.MY_ENV -1 - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ node -p process.env.MY_ENV ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> MY_ENV=2 vite run hello # MY_ENV is pass-through. should hit the cache created in step 1 -$ node -p process.env.MY_ENV (✓ cache hit, replaying) -1 - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 1 cache hits • 0 cache misses -Performance: 100% cache hit rate, ms saved in total - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ node -p process.env.MY_ENV ✓ - → Cache hit - output replayed - ms saved -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> json-edit vite-task.json '_.tasks.hello.passThroughEnvs.push("MY_ENV2")' # add a new pass through env -> MY_ENV=2 vite run hello # cache should be invalidated because passThroughEnvs config changed -$ node -p process.env.MY_ENV (✗ cache miss: envs changed, executing) -2 - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] hello: $ node -p process.env.MY_ENV ✓ - → Cache miss: pass-through env configuration changed from ["MY_ENV"] to ["MY_ENV, MY_ENV2"] -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/packages/cli/snap-tests/change-passthrough-env-config/steps.json b/packages/cli/snap-tests/change-passthrough-env-config/steps.json deleted file mode 100644 index 8b063085..00000000 --- a/packages/cli/snap-tests/change-passthrough-env-config/steps.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "MY_ENV=1 vite run hello", - "MY_ENV=2 vite run hello # MY_ENV is pass-through. should hit the cache created in step 1", - "json-edit vite-task.json '_.tasks.hello.passThroughEnvs.push(\"MY_ENV2\")' # add a new pass through env", - "MY_ENV=2 vite run hello # cache should be invalidated because passThroughEnvs config changed" - ] -} diff --git a/packages/cli/snap-tests/change-passthrough-env-config/vite-task.json b/packages/cli/snap-tests/change-passthrough-env-config/vite-task.json deleted file mode 100644 index 8367fbed..00000000 --- a/packages/cli/snap-tests/change-passthrough-env-config/vite-task.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tasks": { - "hello": { - "command": "node -p process.env.MY_ENV", - "passThroughEnvs": ["MY_ENV"], - "cacheable": true - } - } -} diff --git a/packages/cli/snap-tests/check-oxlint-env/check.js b/packages/cli/snap-tests/check-oxlint-env/check.js deleted file mode 100644 index 84638313..00000000 --- a/packages/cli/snap-tests/check-oxlint-env/check.js +++ /dev/null @@ -1 +0,0 @@ -console.log('OXLINT_TSGOLINT_PATH=%s', process.env.OXLINT_TSGOLINT_PATH); diff --git a/packages/cli/snap-tests/check-oxlint-env/package.json b/packages/cli/snap-tests/check-oxlint-env/package.json deleted file mode 100644 index cbd7ef32..00000000 --- a/packages/cli/snap-tests/check-oxlint-env/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "check-oxlint-env", - "version": "0.0.0", - "private": true, - "scripts": { - "check": "node check.js", - "check-nested": "vite run check" - } -} diff --git a/packages/cli/snap-tests/check-oxlint-env/snap.txt b/packages/cli/snap-tests/check-oxlint-env/snap.txt deleted file mode 100644 index e2116c8f..00000000 --- a/packages/cli/snap-tests/check-oxlint-env/snap.txt +++ /dev/null @@ -1,37 +0,0 @@ -> OXLINT_TSGOLINT_PATH=/path/to/oxlint_tsgolint1 vite run check # get OXLINT_TSGOLINT_PATH env -$ node check.js -OXLINT_TSGOLINT_PATH=/path/to/oxlint_tsgolint1 - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] check-oxlint-env#check: $ node check.js ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -> rm -rf node_modules # clean cache -> OXLINT_TSGOLINT_PATH=/path/to/oxlint_tsgolint2 vite run check-nested # get OXLINT_TSGOLINT_PATH env in nested task -$ node check.js -OXLINT_TSGOLINT_PATH=/path/to/oxlint_tsgolint2 - - - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Vite+ Task Runner • Execution Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Statistics: 1 tasks • 0 cache hits • 1 cache misses -Performance: 0% cache hit rate - -Task Details: -──────────────────────────────────────────────── - [1] check-oxlint-env#check: $ node check.js ✓ - → Cache miss: no previous cache entry found -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/packages/cli/snap-tests/check-oxlint-env/steps.json b/packages/cli/snap-tests/check-oxlint-env/steps.json deleted file mode 100644 index 1bd73ca3..00000000 --- a/packages/cli/snap-tests/check-oxlint-env/steps.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "OXLINT_TSGOLINT_PATH=/path/to/oxlint_tsgolint1 vite run check # get OXLINT_TSGOLINT_PATH env", - "rm -rf node_modules # clean cache", - "OXLINT_TSGOLINT_PATH=/path/to/oxlint_tsgolint2 vite run check-nested # get OXLINT_TSGOLINT_PATH env in nested task" - ] -} diff --git a/packages/cli/snap-tests/command-dev-with-port/package.json b/packages/cli/snap-tests/command-dev-with-port/package.json deleted file mode 100644 index 2c63c085..00000000 --- a/packages/cli/snap-tests/command-dev-with-port/package.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/packages/cli/snap-tests/command-dev-with-port/snap.txt b/packages/cli/snap-tests/command-dev-with-port/snap.txt deleted file mode 100644 index 44d205d2..00000000 --- a/packages/cli/snap-tests/command-dev-with-port/snap.txt +++ /dev/null @@ -1,2 +0,0 @@ -> vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError -RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type number (12312312312). diff --git a/packages/cli/snap-tests/command-dev-with-port/steps.json b/packages/cli/snap-tests/command-dev-with-port/steps.json deleted file mode 100644 index 586d5305..00000000 --- a/packages/cli/snap-tests/command-dev-with-port/steps.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite dev --port 12312312312 2>&1 | grep RangeError # intentionally use an invalid port (exceeds 0-65535) to trigger RangeError" - ] -} diff --git a/packages/cli/snap-tests/command-doc/api-examples.md b/packages/cli/snap-tests/command-doc/api-examples.md deleted file mode 100644 index 691df9cc..00000000 --- a/packages/cli/snap-tests/command-doc/api-examples.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -outline: deep ---- - -# Runtime API Examples - -This page demonstrates usage of some of the runtime APIs provided by VitePress. - -The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: - -```md - - -## Results - -### Theme Data - -
{{ theme }}
- -### Page Data - -
{{ page }}
- -### Page Frontmatter - -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data - -
{{ theme }}
- -### Page Data - -
{{ page }}
- -### Page Frontmatter - -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/packages/cli/snap-tests/command-doc/index.md b/packages/cli/snap-tests/command-doc/index.md deleted file mode 100644 index cd00c096..00000000 --- a/packages/cli/snap-tests/command-doc/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "My Awesome Project" - text: "A VitePress Site" - tagline: My great project tagline - actions: - - theme: brand - text: Markdown Examples - link: /markdown-examples - - theme: alt - text: API Examples - link: /api-examples - -features: - - title: Feature A - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature B - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature C - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit ---- diff --git a/packages/cli/snap-tests/command-doc/markdown-examples.md b/packages/cli/snap-tests/command-doc/markdown-examples.md deleted file mode 100644 index f9258a55..00000000 --- a/packages/cli/snap-tests/command-doc/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -````md -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/packages/cli/snap-tests/command-doc/package.json b/packages/cli/snap-tests/command-doc/package.json deleted file mode 100644 index 70990697..00000000 --- a/packages/cli/snap-tests/command-doc/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "scripts": { - "docs:dev": "vite doc dev", - "docs:build": "vite doc build", - "docs:preview": "vite doc preview" - } -} diff --git a/packages/cli/snap-tests/command-doc/snap.txt b/packages/cli/snap-tests/command-doc/snap.txt deleted file mode 100644 index b3c35c3f..00000000 --- a/packages/cli/snap-tests/command-doc/snap.txt +++ /dev/null @@ -1,24 +0,0 @@ -> vite doc build # build documentation - - vitepress v - -build complete in ms. - - -- building client + server bundles... -✓ building client + server bundles... -- rendering pages... -✓ rendering pages... - -> vite doc build # build documentation again should hit the cache -✓ cache hit, replaying - - vitepress v - -build complete in ms. - - -- building client + server bundles... -✓ building client + server bundles... -- rendering pages... -✓ rendering pages... diff --git a/packages/cli/snap-tests/command-doc/steps.json b/packages/cli/snap-tests/command-doc/steps.json deleted file mode 100644 index b3788bbb..00000000 --- a/packages/cli/snap-tests/command-doc/steps.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, - "commands": [ - "vite doc build # build documentation", - "vite doc build # build documentation again should hit the cache" - ] -} diff --git a/packages/cli/snap-tests/command-helper/package.json b/packages/cli/snap-tests/command-helper/package.json deleted file mode 100644 index 2c63c085..00000000 --- a/packages/cli/snap-tests/command-helper/package.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/packages/cli/snap-tests/command-helper/snap.txt b/packages/cli/snap-tests/command-helper/snap.txt deleted file mode 100644 index c956749a..00000000 --- a/packages/cli/snap-tests/command-helper/snap.txt +++ /dev/null @@ -1,300 +0,0 @@ -> vite lib -h # lib help message -tsdown/ - -Usage: - $ tsdown [...files] - -Commands: - [...files] Bundle files - migrate Migrate from tsup to tsdown - -For more info, run any command with the `--help` flag: - $ tsdown --help - $ tsdown migrate --help - -Options: - -c, --config Use a custom config file - --config-loader Config loader to use: auto, native, unconfig (default: auto) - --no-config Disable config file (default: true) - -f, --format Bundle format: esm, cjs, iife, umd (default: esm) - --clean Clean output directory, --no-clean to disable - --external Mark dependencies as external - --minify Minify output - --debug [feat] Show debug logs - --target Bundle target, e.g "es2015", "esnext" - -l, --logLevel Set log level: info, warn, error, silent - --fail-on-warn Fail on warnings (default: true) - -d, --out-dir Output directory (default: dist) - --treeshake Tree-shake bundle (default: true) - --sourcemap Generate source map (default: false) - --shims Enable cjs and esm shims (default: false) - --platform Target platform (default: node) - --dts Generate dts files - --publint Enable publint (default: false) - --attw Enable Are the types wrong integration (default: false) - --unused Enable unused dependencies check (default: false) - -w, --watch [path] Watch mode - --ignore-watch Ignore custom paths in watch mode - --from-vite [vitest] Reuse config from Vite or Vitest - --report Size report (default: true) - --env.* Define compile-time env variables - --on-success Command to run on success - --copy Copy files to output dir - --public-dir Alias for --copy, deprecated - --tsconfig Set tsconfig path - --unbundle Unbundle mode - -W, --workspace [dir] Enable workspace mode - -F, --filter Filter workspace packages, e.g. /regex/ or substring - --exports Generate export-related metadata for package.json (experimental) - -h, --help Display this message - -v, --version Display version number - - -> vite fmt -h # fmt help message -Usage: [--check | --list-different] [-c=PATH] [--ignore-path=PATH]... [PATH]... - -Output Options - --check Check mode - check if files are formatted - --list-different List mode - list files that would be changed - -Basic Options - -c, --config=PATH Path to the configuration file - -Ignore Options - --ignore-path=PATH Path to ignore file(s). Can be specified multiple times. If not - specified, .gitignore and .prettierignore in the current directory are - used. - --with-node-modules Format code in node_modules directory (skipped by default) - -Miscellaneous - --no-error-on-unmatched-pattern Do not exit with error when pattern is unmatched - --threads=INT Number of threads to use. Set to 1 for using only 1 CPU core - -Available positional items: - PATH Single file, single path or list of paths. If not provided, current - working directory is used. Glob is supported only for exclude patterns - like `'!**/fixtures/*.js'. - -Available options: - -h, --help Prints help information - -V, --version Prints version information - - - -> vite lint -h # lint help message -Usage: [-c=<./.oxlintrc.json>] [PATH]... - -Basic Configuration - -c, --config=<./.oxlintrc.json> Oxlint configuration file (experimental) - * only `.json` extension is supported - * you can use comments in configuration files. - * tries to be compatible with the ESLint v8's format - --tsconfig=<./tsconfig.json> TypeScript `tsconfig.json` path for reading path alias and - project references for import plugin - --init Initialize oxlint configuration with default values - -Allowing / Denying Multiple Lints - Accumulate rules and categories from left to right on the command-line. - For example `-D correctness -A no-debugger` or `-A all -D no-debugger`. - The categories are: - * `correctness` - code that is outright wrong or useless (default). - * `suspicious` - code that is most likely wrong or useless. - * `pedantic` - lints which are rather strict or have occasional false positives. - * `style` - code that should be written in a more idiomatic way. - * `nursery` - new lints that are still under development. - * `restriction` - lints which prevent the use of language and library features. - * `all` - all the categories listed above except nursery. Does not enable plugins - automatically. - -A, --allow=NAME Allow the rule or category (suppress the lint) - -W, --warn=NAME Deny the rule or category (emit a warning) - -D, --deny=NAME Deny the rule or category (emit an error) - -Enable Plugins - --disable-unicorn-plugin Disable unicorn plugin, which is turned on by default - --disable-oxc-plugin Disable oxc unique rules, which is turned on by default - --disable-typescript-plugin Disable TypeScript plugin, which is turned on by default - --import-plugin Enable the experimental import plugin and detect ESM problems. It is - recommended to use along side with the `--tsconfig` option. - --react-plugin Enable react plugin, which is turned off by default - --jsdoc-plugin Enable the experimental jsdoc plugin and detect JSDoc problems - --jest-plugin Enable the Jest plugin and detect test problems - --vitest-plugin Enable the Vitest plugin and detect test problems - --jsx-a11y-plugin Enable the JSX-a11y plugin and detect accessibility problems - --nextjs-plugin Enable the Next.js plugin and detect Next.js problems - --react-perf-plugin Enable the React performance plugin and detect rendering performance - problems - --promise-plugin Enable the promise plugin and detect promise usage problems - --node-plugin Enable the node plugin and detect node usage problems - --regex-plugin Enable the regex plugin and detect regex usage problems - --vue-plugin Enable the vue plugin and detect vue usage problems - -Fix Problems - --fix Fix as many issues as possible. Only unfixed issues are reported in - the output - --fix-suggestions Apply auto-fixable suggestions. May change program behavior. - --fix-dangerously Apply dangerous fixes and suggestions. - -Ignore Files - --ignore-path=PATH Specify the file to use as your .eslintignore - --ignore-pattern=PAT Specify patterns of files to ignore (in addition to those in - .eslintignore) - --no-ignore Disables excluding of files from .eslintignore files, --ignore-path - flags and --ignore-pattern flags - -Handle Warnings - --quiet Disable reporting on warnings, only errors are reported - --deny-warnings Ensure warnings produce a non-zero exit code - --max-warnings=INT Specify a warning threshold, which can be used to force exit with an - error status if there are too many warning-level rule violations in - your project - -Output - -f, --format=ARG Use a specific output format. Possible values: `checkstyle`, - `default`, `github`, `gitlab`, `json`, `junit`, `stylish`, `unix` - -Miscellaneous - --silent Do not display any diagnostics - --threads=INT Number of threads to use. Set to 1 for using only 1 CPU core - --print-config This option outputs the configuration to be used. When present, no - linting is performed and only config-related options are valid. - -Inline Configuration Comments - --report-unused-disable-directives Report directive comments like `// eslint-disable-line` - when no errors would have been reported on that line anyway. - --report-unused-disable-directives-severity=SEVERITY Same as - `--report-unused-disable-directives`, but allows you to specify the - severity level of the reported errors. Only one of these two options - can be used at a time. - -Available positional items: - PATH Single file, single path or list of paths - -Available options: - --rules list all the rules that are currently registered - --disable-nested-config Disables the automatic loading of nested configuration files. - --type-aware Enables rules that require type information. - -h, --help Prints help information - -V, --version Prints version information - - - -> vite build -h # build help message -vite/ - -Usage: - $ vite build [root] - -Options: - --target [string] transpile target (default: 'baseline-widely-available') - --outDir [string] output directory (default: dist) - --assetsDir [string] directory under outDir to place assets in (default: assets) - --assetsInlineLimit [number] static asset base64 inline threshold in bytes (default: 4096) - --ssr [entry] [string] build specified entry for server-side rendering - --sourcemap [output] [boolean | "inline" | "hidden"] output source maps for build (default: false) - --minify [minifier] [boolean | "terser" | "esbuild"] enable/disable minification, or specify minifier to use (default: esbuild) - --manifest [name] [boolean | string] emit build manifest json - --ssrManifest [name] [boolean | string] emit ssr manifest json - --emptyOutDir [boolean] force empty outDir when it's outside of root - -w, --watch [boolean] rebuilds when modules have changed on disk - --app [boolean] same as `builder: {}` - -c, --config [string] use specified config file - --base [string] public base path (default: /) - -l, --logLevel [string] info | warn | error | silent - --clearScreen [boolean] allow/disable clear screen when logging - --configLoader [string] use 'bundle' to bundle the config with esbuild, or 'runner' (experimental) to process it on the fly, or 'native' (experimental) to load using the native runtime (default: bundle) - -d, --debug [feat] [string | boolean] show debug logs - -f, --filter [string] filter debug logs - -m, --mode [string] set env mode - -h, --help Display this message - - -> vite test -h # test help message -vitest/ - WARN: no options were found for your subcommands so we printed the whole output - -Usage: - $ vitest [...filters] - -Commands: - run [...filters] - related [...filters] - watch [...filters] - dev [...filters] - bench [...filters] - init - list [...filters] - [...filters] - -For more info, run any command with the `--help` flag: - $ vitest run --help - $ vitest related --help - $ vitest watch --help - $ vitest dev --help - $ vitest bench --help - $ vitest init --help - $ vitest list --help - $ vitest --help - $ vitest --help --expand-help - -Options: - -v, --version Display version number - -r, --root Root path - -c, --config Path to config file - -u, --update Update snapshot - -w, --watch Enable watch mode - -t, --testNamePattern Run tests with full names matching the specified regexp pattern - --dir Base directory to scan for the test files - --ui Enable UI - --open Open UI automatically (default: !process.env.CI) - --api [port] Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to 51204. Use '--help --api' for more info. - --silent [value] Silent console output from tests. Use 'passed-only' to see logs from failing tests only. - --hideSkippedTests Hide logs for skipped tests - --reporter Specify reporters (default, blob, verbose, dot, json, tap, tap-flat, junit, tree, hanging-process, github-actions) - --outputFile Write test results to a file when supporter reporter is also specified, use cac's dot notation for individual outputs of multiple reporters (example: --outputFile.tap=./tap.txt) - --coverage Enable coverage report. Use '--help --coverage' for more info. - --mode Override Vite mode (default: test or benchmark) - --isolate Run every test file in isolation. To disable isolation, use --no-isolate (default: true) - --globals Inject apis globally - --dom Mock browser API with happy-dom - --browser Run tests in the browser. Equivalent to --browser.enabled (default: false). Use '--help --browser' for more info. - --pool Specify pool, if not running in the browser (default: forks) - --execArgv