Add support for projects managed with Yarn (#13644)
TODO: - [ ] File a PR with Yarn to add Zed to the list of supported IDEs. Fixes: https://github.com/zed-industries/zed/issues/10107 Fixes: https://github.com/zed-industries/zed/issues/13706 Release Notes: - Improved experience in projects using Yarn. Run `yarn dlx @yarnpkg/sdks base` in the root of your project in order to elevate your experience. --------- Co-authored-by: Saurabh <79586784+m4saurabh@users.noreply.github.com>
This commit is contained in:
parent
291d64c803
commit
2727f55772
9 changed files with 320 additions and 24 deletions
177
crates/project/src/yarn.rs
Normal file
177
crates/project/src/yarn.rs
Normal file
|
@ -0,0 +1,177 @@
|
|||
//! This module deals with everything related to path handling for Yarn, the package manager for Web ecosystem.
|
||||
//! Yarn is a bit peculiar, because it references paths within .zip files, which we obviously can't handle.
|
||||
//! It also uses virtual paths for peer dependencies.
|
||||
//!
|
||||
//! Long story short, before we attempt to resolve a path as a "real" path, we try to treat is as a yarn path;
|
||||
//! for .zip handling, we unpack the contents into the temp directory (yes, this is bad, against the spirit of Yarn and what-not)
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Task};
|
||||
use util::ResultExt;
|
||||
|
||||
pub(crate) struct YarnPathStore {
|
||||
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
/// Returns `None` when passed path is a malformed virtual path or it's not a virtual path at all.
|
||||
fn resolve_virtual(path: &Path) -> Option<Arc<Path>> {
|
||||
let components: Vec<_> = path.components().collect();
|
||||
let mut non_virtual_path = PathBuf::new();
|
||||
|
||||
let mut i = 0;
|
||||
let mut is_virtual = false;
|
||||
while i < components.len() {
|
||||
if let Some(os_str) = components[i].as_os_str().to_str() {
|
||||
// Detect the __virtual__ segment
|
||||
if os_str == "__virtual__" {
|
||||
let pop_count = components
|
||||
.get(i + 2)?
|
||||
.as_os_str()
|
||||
.to_str()?
|
||||
.parse::<usize>()
|
||||
.ok()?;
|
||||
|
||||
// Apply dirname operation pop_count times
|
||||
for _ in 0..pop_count {
|
||||
non_virtual_path.pop();
|
||||
}
|
||||
i += 3; // Skip hash and pop_count components
|
||||
is_virtual = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
non_virtual_path.push(&components[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
is_virtual.then(|| Arc::from(non_virtual_path))
|
||||
}
|
||||
|
||||
impl YarnPathStore {
|
||||
pub(crate) fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Model<Self> {
|
||||
cx.new_model(|_| Self {
|
||||
temp_dirs: Default::default(),
|
||||
fs,
|
||||
})
|
||||
}
|
||||
pub(crate) fn process_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
protocol: &str,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Option<(Arc<Path>, Arc<Path>)>> {
|
||||
let mut is_zip = protocol.eq("zip");
|
||||
|
||||
let path: &Path = if let Some(non_zip_part) = path
|
||||
.as_os_str()
|
||||
.as_encoded_bytes()
|
||||
.strip_prefix("/zip:".as_bytes())
|
||||
{
|
||||
// typescript-language-server prepends the paths with zip:, which is messy.
|
||||
is_zip = true;
|
||||
Path::new(OsStr::new(
|
||||
std::str::from_utf8(non_zip_part).expect("Invalid UTF-8"),
|
||||
))
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
let as_virtual = resolve_virtual(&path);
|
||||
let Some(path) = as_virtual.or_else(|| is_zip.then(|| Arc::from(path))) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
if let Some(zip_file) = zip_path(&path) {
|
||||
let zip_file: Arc<Path> = Arc::from(zip_file);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let dir = this
|
||||
.update(&mut cx, |this, _| {
|
||||
this.temp_dirs
|
||||
.get(&zip_file)
|
||||
.map(|temp| temp.path().to_owned())
|
||||
})
|
||||
.ok()?;
|
||||
let zip_root = if let Some(dir) = dir {
|
||||
dir
|
||||
} else {
|
||||
let fs = this.update(&mut cx, |this, _| this.fs.clone()).ok()?;
|
||||
let tempdir = dump_zip(zip_file.clone(), fs).await.log_err()?;
|
||||
let new_path = tempdir.path().to_owned();
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.temp_dirs.insert(zip_file.clone(), tempdir);
|
||||
})
|
||||
.ok()?;
|
||||
new_path
|
||||
};
|
||||
// Rebase zip-path onto new temp path.
|
||||
let as_relative = path.strip_prefix(zip_file).ok()?.into();
|
||||
Some((zip_root.into(), as_relative))
|
||||
})
|
||||
} else {
|
||||
Task::ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn zip_path(path: &Path) -> Option<&Path> {
|
||||
let path_str = path.to_str()?;
|
||||
let zip_end = path_str.find(".zip/")?;
|
||||
let zip_path = &path_str[..zip_end + 4]; // ".zip" is 4 characters long
|
||||
Some(Path::new(zip_path))
|
||||
}
|
||||
|
||||
async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let contents = fs.load_bytes(&path).await?;
|
||||
node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_virtual() {
|
||||
let test_cases = vec![
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat",
|
||||
Some(Path::new("/path/subpath/to/file.dat")),
|
||||
),
|
||||
("/path/to/nonvirtual/", None),
|
||||
("/path/to/malformed/__virtual__", None),
|
||||
("/path/to/malformed/__virtual__/a0b1c2d3", None),
|
||||
(
|
||||
"/path/to/malformed/__virtual__/a0b1c2d3/this-should-be-a-number",
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
let input_path = Path::new(input);
|
||||
let resolved_path = resolve_virtual(input_path);
|
||||
assert_eq!(resolved_path.as_deref(), expected);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue