Skip to content

Commit f4cec33

Browse files
MarshallOfSoundJamieMason
authored andcommitted
feat(core): add support for yarn@v4 link: syntax
Closes #293
1 parent c80a54d commit f4cec33

File tree

7 files changed

+85
-0
lines changed

7 files changed

+85
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"Max Rohde (https://github.com/mxro)",
3131
"Michał Warać (https://github.com/auto200)",
3232
"Nick Saunders (https://github.com/nsaunders)",
33+
"Samuel Attard (https://github.com/MarshallOfSound)",
3334
"Siraj (https://github.com/Syhner)",
3435
"Steve Beaugé (https://github.com/stevebeauge)",
3536
"Stuart Knightley (https://github.com/Stuk)",

site/src/content/docs/reference/specifier-types.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ A dependency that resolves to the latest available version. Includes wildcard pa
3939

4040
Examples: `latest`, `*`
4141

42+
### link
43+
44+
A dependency that references a local package by path.
45+
46+
Example: `link:../package-a`
47+
4248
### major
4349

4450
A dependency specifier that only indicates the major version number.

src/specifier.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pub enum Specifier {
8484
Tag(Raw),
8585
Unsupported(Raw),
8686
Url(Raw),
87+
Link(Raw),
8788
WorkspaceProtocol(WorkspaceProtocol),
8889
}
8990

@@ -132,6 +133,9 @@ impl Specifier {
132133
if first_char == 'f' && value.starts_with("file:") {
133134
return Self::File(raw::Raw { raw: value.to_string() });
134135
}
136+
if first_char == 'l' && value.starts_with("link:") {
137+
return Self::Link(raw::Raw { raw: value.to_string() });
138+
}
135139
if first_char == 'h' && (value.starts_with("http://") || value.starts_with("https://")) {
136140
return Self::Url(raw::Raw { raw: value.to_string() });
137141
}
@@ -324,6 +328,7 @@ impl Specifier {
324328
Self::ComplexSemver(s) => Self::ComplexSemver(s.with_range(range)),
325329
Self::File(s) => Self::File(s.with_range(range)),
326330
Self::Git(s) => Self::Git(s.with_range(range)),
331+
Self::Link(s) => Self::Link(s.with_range(range)),
327332
Self::None => Self::None,
328333
Self::Tag(s) => Self::Tag(s.with_range(range)),
329334
Self::Unsupported(s) => Self::Unsupported(s.with_range(range)),
@@ -342,6 +347,7 @@ impl Specifier {
342347
Self::ComplexSemver(s) => Self::ComplexSemver(s.with_semver(semver)),
343348
Self::File(s) => Self::File(s.with_semver(semver)),
344349
Self::Git(s) => Self::Git(s.with_semver(semver)),
350+
Self::Link(s) => Self::Link(s.with_semver(semver)),
345351
Self::None => Self::None,
346352
Self::Tag(s) => Self::Tag(s.with_semver(semver)),
347353
Self::Unsupported(s) => Self::Unsupported(s.with_semver(semver)),
@@ -357,6 +363,7 @@ impl Specifier {
357363
Self::ComplexSemver(inner) => inner.raw.clone(),
358364
Self::File(inner) => inner.raw.clone(),
359365
Self::Git(inner) => inner.raw.clone(),
366+
Self::Link(inner) => inner.raw.clone(),
360367
Self::None => "".to_string(),
361368
Self::Tag(inner) => inner.raw.clone(),
362369
Self::Unsupported(inner) => inner.raw.clone(),
@@ -388,6 +395,7 @@ impl Specifier {
388395
Self::ComplexSemver(_) => "range-complex",
389396
Self::File(_) => "file",
390397
Self::Git(_) => "git",
398+
Self::Link(_) => "link",
391399
Self::None => "missing",
392400
Self::Tag(_) => "tag",
393401
Self::Unsupported(_) => "unsupported",
@@ -427,6 +435,10 @@ impl Specifier {
427435
matches!(self, Self::WorkspaceProtocol(_))
428436
}
429437

438+
pub fn is_link(&self) -> bool {
439+
matches!(self, Self::Link(_))
440+
}
441+
430442
/// Are both specifiers on eg. "-alpha", or neither have a release channel?
431443
pub fn has_same_release_channel_as(&self, other: &Specifier) -> bool {
432444
if let (Some(a), Some(b)) = (self.get_node_version(), other.get_node_version()) {

src/specifier/regexes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ lazy_static! {
6363
pub static ref ALIAS: Regex = Regex::new(r"^npm:.+").unwrap();
6464
/// "file:"
6565
pub static ref FILE: Regex = Regex::new(r"^file:").unwrap();
66+
/// "link:"
67+
pub static ref LINK: Regex = Regex::new(r"^link:").unwrap();
6668
/// "workspace:"
6769
pub static ref WORKSPACE_PROTOCOL: Regex = Regex::new(r"^workspace:").unwrap();
6870
/// "https://"

src/specifier_test.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,19 @@ fn file_paths() {
372372
}
373373
}
374374

375+
#[test]
376+
fn links() {
377+
let cases: Vec<&str> = vec!["link:../foo", "link:path/to/foo"];
378+
for value in cases {
379+
match Specifier::new(value, None) {
380+
Specifier::Link(actual) => {
381+
assert_eq!(actual.raw, value)
382+
}
383+
_ => panic!("Expected Link"),
384+
}
385+
}
386+
}
387+
375388
#[test]
376389
fn urls() {
377390
let cases: Vec<&str> = vec![

src/visit_packages/preferred_semver.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ pub fn visit(dependency: &Dependency, ctx: &Context) {
4444
return;
4545
}
4646
debug!("{L4}it depends on the local instance");
47+
if instance.descriptor.specifier.is_link() {
48+
debug!("{L5}it is using the link specifier");
49+
debug!("{L6}mark as satisfying local");
50+
instance.mark_valid(ValidInstance::SatisfiesLocal, &instance.descriptor.specifier);
51+
return;
52+
}
4753
if instance.descriptor.specifier.is_workspace_protocol() {
4854
debug!("{L5}it is using the workspace protocol");
4955
if !ctx.config.rcfile.strict {

src/visit_packages/preferred_semver_test.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,51 @@ mod local {
466466
},
467467
]);
468468
}
469+
470+
#[test]
471+
fn instance_is_linked() {
472+
let ctx = TestBuilder::new()
473+
.with_packages(vec![
474+
json!({
475+
"name": "package-a",
476+
"version": "1.0.0"
477+
}),
478+
json!({
479+
"name": "package-b",
480+
"version": "2.0.0",
481+
"dependencies": {
482+
"package-a": "link:../package-a"
483+
}
484+
}),
485+
])
486+
.build_and_visit_packages();
487+
expect(&ctx).to_have_instances(vec![
488+
ExpectedInstance {
489+
state: InstanceState::valid(IsLocalAndValid),
490+
dependency_name: "package-b",
491+
id: "package-b in /version of package-b",
492+
actual: "2.0.0",
493+
expected: Some("2.0.0"),
494+
overridden: None,
495+
},
496+
ExpectedInstance {
497+
state: InstanceState::valid(IsLocalAndValid),
498+
dependency_name: "package-a",
499+
id: "package-a in /version of package-a",
500+
actual: "1.0.0",
501+
expected: Some("1.0.0"),
502+
overridden: None,
503+
},
504+
ExpectedInstance {
505+
state: InstanceState::valid(IsLocalAndValid),
506+
dependency_name: "package-a",
507+
id: "package-a in /dependencies of package-b",
508+
actual: "link:../package-a",
509+
expected: Some("link:../package-a"),
510+
overridden: None,
511+
},
512+
]);
513+
}
469514
}
470515

471516
mod highest_or_lowest {

0 commit comments

Comments
 (0)