Skip to content

Commit a491ea9

Browse files
committed
API improvements
1 parent d51fce6 commit a491ea9

File tree

5 files changed

+195
-31
lines changed

5 files changed

+195
-31
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pnp"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
license = "BSD-2-Clause"
66
description = "Resolution primitives for Yarn PnP"

README.md

+122-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,124 @@
11
# `pnp-rs`
22

3-
This crate implements the Yarn Plug'n'Play [resolution algorithms](https://yarnpkg.com/advanced/pnp-spec).
3+
This crate implements the Yarn Plug'n'Play [resolution algorithms](https://yarnpkg.com/advanced/pnp-spec) for Rust so that it can be easily reused within Rust-based tools. It also includes utilities allowing to transparently read files from within zip archives.
4+
5+
## Install
6+
7+
```
8+
cargo add pnp
9+
```
10+
11+
## Resolution
12+
13+
```rust
14+
fn example() {
15+
let manifest
16+
= load_pnp_manifest(".pnp.cjs").unwrap();
17+
18+
let host = ResolutionHost {
19+
find_pnp_manifest: Box::new(move |_| Ok(Some(manifest.clone()))),
20+
..Default::default()
21+
};
22+
23+
let config = ResolutionConfig {
24+
host,
25+
..Default::default()
26+
};
27+
28+
let resolution = resolve_to_unqualified(
29+
"lodash/cloneDeep",
30+
std::path::PathBuf::from("/path/to/index.js"),
31+
&config,
32+
);
33+
34+
match resolution {
35+
Ok(Resolution::Package(path, subpath)) => {
36+
// path = "/path/to/lodash.zip"
37+
// subpath = "cloneDeep"
38+
},
39+
Ok(Resolution::Specifier(specifier)) => {
40+
// This is returned when the PnP resolver decides that it shouldn't
41+
// handle the resolution for this particular specifier. In that case,
42+
// the specifier should be forwarded to the default resolver.
43+
},
44+
Err(err) => {
45+
// An error happened during the resolution. Falling back to the default
46+
// resolver isn't recommended.
47+
},
48+
};
49+
}
50+
```
51+
52+
## Filesystem utilities
53+
54+
While PnP only deals with the resolution, not the filesystem, the file maps generated by Yarn rely on virtual filesystem layers for two reasons:
55+
56+
- [Virtual packages](https://yarnpkg.com/advanced/lexicon#virtual-package), which require a same package to have different paths to account for different set of dependencies (this only happens for packages that list peer dependencies)
57+
58+
- Zip storage, which Yarn uses so the installed files never have to be unpacked from their archives, leading to faster installs and fewer risks of cache corruption.
59+
60+
To make it easier to work with these virtual filesystems, the `pnp` crate also includes a `VPath` enum that lets you resolve virtual paths, and a set of zip manipulation utils (`open_zip_via_read` by default, and `open_zip_via_mmap` if the `mmap` feature is enabled).
61+
62+
```rust
63+
use pnp::fs::{VPath, open_zip_via_read};
64+
65+
fn read_file(p: PathBuf) -> std::io::Result<String> {
66+
match VPath::from(&p).unwrap() {
67+
VPath::Virtual(info) => {
68+
let physical_path
69+
= info.physical_base_path();
70+
71+
match &info.zip_path {
72+
// The path was virtual and stored within a zip file; we need to read from the zip file
73+
// Note that this opens the zip file every time, which is expensive; we'll see how to optimize that
74+
Some(zip_path) => open_zip_via_read(&physical_path)
75+
.unwrap()
76+
.read_to_string(&zip_path),
77+
78+
// The path was virtual but not a zip file; we just need to read from the provided location
79+
None => std::fs::read_to_string(info.physical_base_path())
80+
}
81+
},
82+
83+
// Nothing special to do, it's a regular path
84+
VPath::Native(p) => {
85+
std::fs::read_to_string(&p)
86+
},
87+
}
88+
}
89+
```
90+
91+
## Cache reuse
92+
93+
Opening and dropping a zip archive for every single file access would be expensive. To avoid that, `pnp-rs` provides an helper class called `LruZipCache` which lets you abstract away the zip opening and closing, and only keep the most recently used archives open.
94+
95+
```rust
96+
use pnp::fs::{VPath, LruZipCache, open_zip_via_read};
97+
98+
const ZIP_CACHE: Lazy<LruZipCache<Vec<u8>>> = Lazy::new(|| {
99+
// It'll keep the last 50 zip archives open
100+
LruZipCache::new(50, open_zip_via_read)
101+
});
102+
103+
fn read_file(p: PathBuf) -> std::io::Result<String> {
104+
match VPath::from(&p).unwrap() {
105+
VPath::Virtual(info) => {
106+
let physical_path
107+
= info.physical_base_path();
108+
109+
match &info.zip_path {
110+
// The path was virtual and stored within a zip file; we need to read from the zip file
111+
Some(zip_path) => ZIP_CACHE.read_to_string(info.physical_base_path()),
112+
113+
// The path was virtual but not a zip file; we just need to read from the provided location
114+
None => std::fs::read_to_string(info.physical_base_path())
115+
}
116+
},
117+
118+
// Nothing special to do, it's a regular path
119+
VPath::Native(p) => {
120+
std::fs::read_to_string(&p)
121+
},
122+
}
123+
}
124+
```

src/fs.rs

+33-28
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ pub enum VPath {
2929
Native(PathBuf),
3030
}
3131

32+
impl VPath {
33+
pub fn from(p: &Path) -> std::io::Result<VPath> {
34+
vpath(p)
35+
}
36+
}
37+
3238
#[derive(thiserror::Error, Debug)]
3339
pub enum Error {
3440
#[error("Entry not found")]
@@ -60,7 +66,7 @@ fn io_bytes_to_str(vec: &[u8]) -> Result<&str, std::io::Error> {
6066
}
6167

6268
#[cfg(feature = "mmap")]
63-
pub fn open_zip_via_mmap(p: &Path) -> Result<Zip<mmap_rs::Mmap>, std::io::Error> {
69+
pub fn open_zip_via_mmap<P: AsRef<Path>>(p: P) -> Result<Zip<mmap_rs::Mmap>, std::io::Error> {
6470
let file = fs::File::open(p)?;
6571

6672
let mmap_builder = mmap_rs::MmapOptions::new(file.metadata().unwrap().len().try_into().unwrap())
@@ -78,8 +84,7 @@ pub fn open_zip_via_mmap(p: &Path) -> Result<Zip<mmap_rs::Mmap>, std::io::Error>
7884

7985
Ok(zip)
8086
}
81-
82-
pub fn open_zip_via_read(p: &Path) -> Result<Zip<Vec<u8>>, std::io::Error> {
87+
pub fn open_zip_via_read<P: AsRef<Path>>(p: P) -> Result<Zip<Vec<u8>>, std::io::Error> {
8388
let data = std::fs::read(p)?;
8489

8590
let zip = Zip::new(data)
@@ -89,26 +94,26 @@ pub fn open_zip_via_read(p: &Path) -> Result<Zip<Vec<u8>>, std::io::Error> {
8994
}
9095

9196
pub trait ZipCache<Storage>
92-
where Storage : AsRef<[u8]> + Send + Sync {
93-
fn act<T, F : FnOnce(&Zip<Storage>) -> T>(&self, p: &Path, cb: F) -> Result<T, std::io::Error>;
97+
where Storage: AsRef<[u8]> + Send + Sync {
98+
fn act<T, P: AsRef<Path>, F : FnOnce(&Zip<Storage>) -> T>(&self, p: P, cb: F) -> Result<T, std::io::Error>;
9499

95-
fn canonicalize(&self, zip_path: &Path, sub: &str) -> Result<PathBuf, std::io::Error>;
100+
fn canonicalize<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, sub: S) -> Result<PathBuf, std::io::Error>;
96101

97-
fn is_dir(&self, zip_path: &Path, sub: &str) -> bool;
98-
fn is_file(&self, zip_path: &Path, sub: &str) -> bool;
102+
fn is_dir<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, sub: S) -> bool;
103+
fn is_file<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, sub: S) -> bool;
99104

100-
fn read(&self, zip_path: &Path, sub: &str) -> Result<Vec<u8>, std::io::Error>;
101-
fn read_to_string(&self, zip_path: &Path, sub: &str) -> Result<String, std::io::Error>;
105+
fn read<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, sub: S) -> Result<Vec<u8>, std::io::Error>;
106+
fn read_to_string<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, sub: S) -> Result<String, std::io::Error>;
102107
}
103108

104109
pub struct LruZipCache<Storage>
105-
where Storage : AsRef<[u8]> + Send + Sync {
110+
where Storage: AsRef<[u8]> + Send + Sync {
106111
lru: concurrent_lru::sharded::LruCache<PathBuf, Zip<Storage>>,
107112
open: fn(&Path) -> std::io::Result<Zip<Storage>>,
108113
}
109114

110115
impl<Storage> LruZipCache<Storage>
111-
where Storage : AsRef<[u8]> + Send + Sync {
116+
where Storage: AsRef<[u8]> + Send + Sync {
112117
pub fn new(n: u64, open: fn(&Path) -> std::io::Result<Zip<Storage>>) -> LruZipCache<Storage> {
113118
LruZipCache {
114119
lru: concurrent_lru::sharded::LruCache::new(n),
@@ -118,39 +123,39 @@ where Storage : AsRef<[u8]> + Send + Sync {
118123
}
119124

120125
impl<Storage> ZipCache<Storage> for LruZipCache<Storage>
121-
where Storage : AsRef<[u8]> + Send + Sync {
122-
fn act<T, F : FnOnce(&Zip<Storage>) -> T>(&self, p: &Path, cb: F) -> Result<T, std::io::Error> {
123-
let zip = self.lru.get_or_try_init(p.to_path_buf(), 1, |p| {
126+
where Storage: AsRef<[u8]> + Send + Sync {
127+
fn act<T, P: AsRef<Path>, F: FnOnce(&Zip<Storage>) -> T>(&self, p: P, cb: F) -> Result<T, std::io::Error> {
128+
let zip = self.lru.get_or_try_init(p.as_ref().to_path_buf(), 1, |p| {
124129
(self.open)(&p)
125130
})?;
126131

127132
Ok(cb(zip.value()))
128133
}
129134

130-
fn canonicalize(&self, zip_path: &Path, sub: &str) -> Result<PathBuf, std::io::Error> {
135+
fn canonicalize<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, sub: S) -> Result<PathBuf, std::io::Error> {
131136
let res = std::fs::canonicalize(zip_path)?;
132137

133-
Ok(res.join(sub))
138+
Ok(res.join(sub.as_ref()))
134139
}
135140

136-
fn is_dir(&self, zip_path: &Path, p: &str) -> bool {
137-
self.act(zip_path, |zip| zip.is_dir(p)).unwrap_or(false)
141+
fn is_dir<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, p: S) -> bool {
142+
self.act(zip_path, |zip| zip.is_dir(p.as_ref())).unwrap_or(false)
138143
}
139144

140-
fn is_file(&self, zip_path: &Path, p: &str) -> bool {
141-
self.act(zip_path, |zip| zip.is_file(p)).unwrap_or(false)
145+
fn is_file<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, p: S) -> bool {
146+
self.act(zip_path, |zip| zip.is_file(p.as_ref())).unwrap_or(false)
142147
}
143148

144-
fn read(&self, zip_path: &Path, p: &str) -> Result<Vec<u8>, std::io::Error> {
145-
self.act(zip_path, |zip| zip.read(p))?
149+
fn read<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, p: S) -> Result<Vec<u8>, std::io::Error> {
150+
self.act(zip_path, |zip| zip.read(p.as_ref()))?
146151
}
147152

148-
fn read_to_string(&self, zip_path: &Path, p: &str) -> Result<String, std::io::Error> {
149-
self.act(zip_path, |zip| zip.read_to_string(p))?
153+
fn read_to_string<P: AsRef<Path>, S: AsRef<str>>(&self, zip_path: P, p: S) -> Result<String, std::io::Error> {
154+
self.act(zip_path, |zip| zip.read_to_string(p.as_ref()))?
150155
}
151156
}
152157

153-
pub fn split_zip(p_bytes: &[u8]) -> (&[u8], Option<&[u8]>) {
158+
fn split_zip(p_bytes: &[u8]) -> (&[u8], Option<&[u8]>) {
154159
lazy_static! {
155160
static ref ZIP_RE: Regex = Regex::new(r"\.zip").unwrap();
156161
}
@@ -179,7 +184,7 @@ pub fn split_zip(p_bytes: &[u8]) -> (&[u8], Option<&[u8]>) {
179184
(p_bytes, None)
180185
}
181186

182-
pub fn split_virtual(p_bytes: &[u8]) -> std::io::Result<(usize, Option<(usize, usize)>)> {
187+
fn split_virtual(p_bytes: &[u8]) -> std::io::Result<(usize, Option<(usize, usize)>)> {
183188
lazy_static! {
184189
static ref VIRTUAL_RE: Regex = Regex::new("(?:^|/)((?:\\$\\$virtual|__virtual__)/(?:[^/]+)-[a-f0-9]+/([0-9]+)/)").unwrap();
185190
}
@@ -195,7 +200,7 @@ pub fn split_virtual(p_bytes: &[u8]) -> std::io::Result<(usize, Option<(usize, u
195200
Ok((p_bytes.len(), None))
196201
}
197202

198-
pub fn vpath(p: &Path) -> std::io::Result<VPath> {
203+
fn vpath(p: &Path) -> std::io::Result<VPath> {
199204
let p_str = arca::path::normalize_path(
200205
&p.as_os_str()
201206
.to_string_lossy()

src/lib_tests.rs

+38
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,44 @@ mod tests {
2323
use crate::{init_pnp_manifest, load_pnp_manifest, resolve_to_unqualified, ResolutionHost};
2424
use super::*;
2525

26+
#[test]
27+
fn example() {
28+
let manifest
29+
= load_pnp_manifest("data/pnp-yarn-v3.cjs").unwrap();
30+
31+
let host = ResolutionHost {
32+
find_pnp_manifest: Box::new(move |_| Ok(Some(manifest.clone()))),
33+
..Default::default()
34+
};
35+
36+
let config = ResolutionConfig {
37+
host,
38+
..Default::default()
39+
};
40+
41+
let resolution = resolve_to_unqualified(
42+
"lodash/cloneDeep",
43+
std::path::PathBuf::from("/path/to/file"),
44+
&config,
45+
);
46+
47+
match resolution {
48+
Ok(Resolution::Package(path, subpath)) => {
49+
// path = "/path/to/lodash.zip"
50+
// subpath = "cloneDeep"
51+
},
52+
Ok(Resolution::Specifier(specifier)) => {
53+
// This is returned when the PnP resolver decides that it shouldn't
54+
// handle the resolution for this particular specifier. In that case,
55+
// the specifier should be forwarded to the default resolver.
56+
},
57+
Err(err) => {
58+
// An error happened during the resolution. Falling back to the default
59+
// resolver isn't recommended.
60+
},
61+
};
62+
}
63+
2664
#[test]
2765
fn test_load_pnp_manifest() {
2866
load_pnp_manifest("data/pnp-yarn-v3.cjs")

0 commit comments

Comments
 (0)