Skip to content

Commit 2ba6195

Browse files
authored
Implements support for a Node.js integration (#158)
* Implements support for a Node.js integration * Yields an error when no variants match * Style * Adds tests
1 parent e7848f0 commit 2ba6195

48 files changed

Lines changed: 1346 additions & 444 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
category: concepts
3+
slug: concepts/nvm
4+
title: Node.js versioning
5+
description: Pin the version of Node.js used in your application
6+
sidebar:
7+
order: 3
8+
---
9+
10+
One of the subtle causes of "works on my machine" bugs comes from differences in Node.js versions between developers. While tools like nvm, fnm, or Volta help manage local Node.js installations, they require each team member to manually configure their environment. Yarn takes a different approach by letting you declare Node.js as a project dependency, ensuring everyone uses exactly the same version without any extra setup.
11+
12+
## The `@builtin/node` dependency
13+
14+
Yarn provides a special `@builtin/node` package that you can add to your dependencies just like any other package. When installed, Yarn will download the appropriate Node.js binary for your platform directly from [nodejs.org](https://nodejs.org/) and make it available to your project.
15+
16+
```json
17+
{
18+
"name": "my-app",
19+
"dependencies": {
20+
"@builtin/node": "^22.0.0"
21+
}
22+
}
23+
```
24+
25+
The range here works similarly to semver ranges - Yarn will resolve it to the highest available Node.js version that satisfies your constraint. Once resolved, this version gets locked in your `yarn.lock` file, guaranteeing that every developer and CI environment uses the exact same Node.js release.
26+
27+
## Why manage Node.js through Yarn?
28+
29+
There are several advantages to managing Node.js as a dependency:
30+
31+
- **Zero configuration** - CI and team members don't need to install nvm or any other version manager. Just `yarn install` and they're ready to go.
32+
33+
- **Version locking** - The exact Node.js version is recorded and cached along with all other dependencies in your project.
34+
35+
- **Per-project versions** - Different projects can use different Node.js versions without any manual switching. Yarn handles it automatically.
36+
37+
- **Per-workspace versions** - You can easily override the Node.js version to use for a single workspace or a set of workspaces through [profiles](/concepts/profiles).
38+
39+
## Using the managed Node.js
40+
41+
Once installed, the managed Node.js binary is available through the `node` binary that Yarn injects into your environment. You can use it in several ways:
42+
43+
### Through Yarn scripts
44+
45+
Package scripts automatically use the project's Node.js version:
46+
47+
```json
48+
{
49+
"scripts": {
50+
"start": "node server.js"
51+
}
52+
}
53+
```
54+
55+
### Through `yarn node` / `yarn exec`
56+
57+
Both commands will run Node.js with the correct environment setup:
58+
59+
```bash
60+
yarn node --version
61+
yarn node script.js
62+
yarn exec node --version
63+
```
64+
65+
## Monorepo support
66+
67+
In a monorepo, you typically want all workspaces to use the same Node.js version. Rather than adding `@builtin/node` to each workspace's `package.json`, you can use [workspace profiles](/concepts/profiles) to declare it once:
68+
69+
```yaml
70+
workspaceProfiles:
71+
default:
72+
devDependencies:
73+
"@builtin/node": "builtin:^22.0.0"
74+
```
75+
76+
Since the `default` profile is automatically applied to all workspaces, every package in your monorepo will use the same Node.js version without any additional configuration. This keeps your Node.js version centralized and easy to update.
77+
78+
## Platform support
79+
80+
The `@builtin/node` package automatically downloads the correct binary for your operating system and architecture. Currently supported platforms include:
81+
82+
- Linux (x64, arm64)
83+
- macOS (x64, arm64)
84+
85+
When working in a team with mixed platforms, Yarn will store metadata about all required platform variants in the lockfile, but each developer will only downloads the binary they need for their platform (configurable through `supportedArchitectures`).
86+
87+
This ensures the lockfile remains consistent across the entire team while keeping installs as lightweight as possible.

documentation/src/content/docs/getting-started/migrating/breaking-changes.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@ description: A detailed explanation of the breaking changes between two versions
1111
This document lists the **intended breaking changes**. Yarn 6 being still in development, some features are still missing and will be implemented before we publish the first stable release.
1212
:::
1313

14-
### Plugins
14+
### Not implemented
1515

16-
A special note for plugins, which aren't implemented **yet**.
16+
Reimplementing a codebase comes with challenges, and the two following features haven't been implemented **yet**. We plan to address them before the first stable release:
1717

18-
Various other projects (Biome, Oxc, etc) are experimenting on that topic, and we prefer to wait before focusing on that so we can leverage their researches before building our own solutions.
18+
- Plugins; various other projects (Biome, Oxc, etc) are experimenting on that topic, and we prefer to let them clear the way before building our own solutions.
19+
20+
- Windows support; we already have a path abstraction to prepare for this task, but no tests haven't been run on Windows yet and various things are likely broken. We recommend WSL as a workaround.
1921

2022
### Important features
2123

2224
Some new features have been implemented. They are not "breaking changes" per se, but may make some of your existing tooling obsolete, so be sure to take a look at them:
2325

26+
- [Native Node.js version management](/concepts/nvm), which allows Yarn to treat Node.js as any other dependency, removing the need for third-party tools like nvm / fnm / volta / ...
27+
2428
- [Workspace profiles](/concepts/profiles), which let you definite set of dependencies to reuse in your workspaces
2529

2630
### Lockfile
@@ -56,3 +60,9 @@ Some new features have been implemented. They are not "breaking changes" per se,
5660
### Deprecations
5761

5862
- The `.pnp.cjs` file isn't generated with the `+x` flag anymore.
63+
64+
- Behavior inherited from npm, packages are currently allowed to omit listing dependencies on `node-gyp` if the package happens to contain a `binding.gyp` file.
65+
66+
This behavior is unsafe as the only reasonable thing the package manager can do is to imply a dependency on `*`, meaning there are no guarantees as to the version of `node-gyp` projects would end up using.
67+
68+
This undocumented behavior is now **deprecated** and will be removed in a future release. Popular packages that already rely on it will get an hardcoded package extension so they keep working, but the implicit `node-gyp` dependency won't be applied to any other package going forward.

packages/zpm-config/schema.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@
125125
}
126126
}
127127
},
128+
"nodeDistUrl": {
129+
"type": "string",
130+
"description": "The URL to use for downloading Node.js distributions",
131+
"default": "https://nodejs.org/dist"
132+
},
128133
"nodeLinker": {
129134
"type": "crate::NodeLinker",
130135
"description": "The linker to use for node_modules",
@@ -299,21 +304,21 @@
299304
"type": "array",
300305
"description": "List of CPU architectures to cover.",
301306
"items": {
302-
"type": "crate::Cpu"
307+
"type": "zpm_utils::Cpu"
303308
}
304309
},
305310
"libc": {
306311
"type": "array",
307312
"description": "The list of standard C libraries to cover.",
308313
"items": {
309-
"type": "crate::Libc"
314+
"type": "zpm_utils::Libc"
310315
}
311316
},
312317
"os": {
313318
"type": "array",
314319
"description": "The list of operating systems to cover.",
315320
"items": {
316-
"type": "crate::Os"
321+
"type": "zpm_utils::Os"
317322
}
318323
}
319324
}

packages/zpm-config/src/lib.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{collections::BTreeMap, fmt::Display, ops::Deref, sync::Arc};
22

33
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
4-
use zpm_utils::{AbstractValue, Container, DataType, FromFileString, IoResultExt, Path, RawString, Serialized, ToFileString, ToHumanString, tree};
4+
use zpm_utils::{AbstractValue, Container, Cpu, DataType, FromFileString, IoResultExt, Libc, Os, Path, RawString, Serialized, System, ToFileString, ToHumanString, tree};
55

66
#[derive(Debug, Clone)]
77
pub struct ConfigurationContext {
@@ -698,6 +698,66 @@ macro_rules! merge_settings {
698698

699699
include!(concat!(env!("OUT_DIR"), "/schema.rs"));
700700

701+
impl SupportedArchitectures {
702+
pub fn to_systems(&self) -> Vec<System> {
703+
let mut systems
704+
= Vec::new();
705+
706+
let current
707+
= System::from_current();
708+
709+
let cpus = if self.cpu.is_empty() {
710+
vec![&Cpu::Current]
711+
} else {
712+
self.cpu.iter().map(|c| &c.value).collect()
713+
};
714+
715+
let os = if self.os.is_empty() {
716+
vec![&Os::Current]
717+
} else {
718+
self.os.iter().map(|o| &o.value).collect()
719+
};
720+
721+
let libc = if self.libc.is_empty() {
722+
vec![&Libc::Current]
723+
} else {
724+
self.libc.iter().map(|l| &l.value).collect()
725+
};
726+
727+
for &cpu in &cpus {
728+
for &os in &os {
729+
for &libc in &libc {
730+
let arch = if cpu == &Cpu::Current {
731+
current.arch.clone()
732+
} else {
733+
Some(cpu.clone())
734+
};
735+
736+
let os = if os == &Os::Current {
737+
current.os.clone()
738+
} else {
739+
Some(os.clone())
740+
};
741+
742+
let libc = if libc == &Libc::Current {
743+
current.libc.clone()
744+
} else {
745+
Some(libc.clone())
746+
};
747+
748+
systems.push(System {
749+
arch,
750+
os,
751+
libc,
752+
});
753+
}
754+
}
755+
}
756+
757+
systems
758+
}
759+
}
760+
701761
pub struct Configuration {
702762
pub settings: Settings,
703763
pub user_config_path: Option<Path>,
@@ -846,11 +906,11 @@ merge_settings!(zpm_primitives::Reference, |s: &str| FromFileString::from_file_s
846906

847907
merge_settings!(zpm_semver::RangeKind, |s: &str| FromFileString::from_file_string(s).unwrap());
848908

849-
merge_settings!(zpm_utils::Secret<String>, |s: &str| FromFileString::from_file_string(s).unwrap());
909+
merge_settings!(zpm_utils::Cpu, |s: &str| FromFileString::from_file_string(s).unwrap());
850910
merge_settings!(zpm_utils::Glob, |s: &str| FromFileString::from_file_string(s).unwrap());
911+
merge_settings!(zpm_utils::Libc, |s: &str| FromFileString::from_file_string(s).unwrap());
912+
merge_settings!(zpm_utils::Os, |s: &str| FromFileString::from_file_string(s).unwrap());
913+
merge_settings!(zpm_utils::Secret<String>, |s: &str| FromFileString::from_file_string(s).unwrap());
851914

852915
merge_settings!(crate::types::NodeLinker, |s: &str| FromFileString::from_file_string(s).unwrap());
853916
merge_settings!(crate::types::PnpFallbackMode, |s: &str| FromFileString::from_file_string(s).unwrap());
854-
merge_settings!(crate::types::Cpu, |s: &str| FromFileString::from_file_string(s).unwrap());
855-
merge_settings!(crate::types::Libc, |s: &str| FromFileString::from_file_string(s).unwrap());
856-
merge_settings!(crate::types::Os, |s: &str| FromFileString::from_file_string(s).unwrap());

packages/zpm-config/src/types.rs

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,57 +28,3 @@ pub enum PnpFallbackMode {
2828
#[literal("all")]
2929
All,
3030
}
31-
32-
#[zpm_enum(error = ConfigurationError, or_else = |s| Err(ConfigurationError::EnumError(s.to_string())))]
33-
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
34-
pub enum Cpu {
35-
#[literal("current")]
36-
Current,
37-
38-
#[literal("ia32")]
39-
I386,
40-
41-
#[literal("x64")]
42-
X86_64,
43-
44-
#[literal("arm64")]
45-
Aarch64,
46-
47-
#[fallback]
48-
Other(String),
49-
}
50-
51-
#[zpm_enum(error = ConfigurationError, or_else = |s| Err(ConfigurationError::EnumError(s.to_string())))]
52-
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
53-
pub enum Libc {
54-
#[literal("current")]
55-
Current,
56-
57-
#[literal("glibc")]
58-
Glibc,
59-
60-
#[literal("musl")]
61-
Musl,
62-
63-
#[fallback]
64-
Other(String),
65-
}
66-
67-
#[zpm_enum(error = ConfigurationError, or_else = |s| Err(ConfigurationError::EnumError(s.to_string())))]
68-
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
69-
pub enum Os {
70-
#[literal("current")]
71-
Current,
72-
73-
#[literal("darwin")]
74-
MacOS,
75-
76-
#[literal("linux")]
77-
Linux,
78-
79-
#[literal("win32")]
80-
Windows,
81-
82-
#[fallback]
83-
Other(String),
84-
}

packages/zpm-formats/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ impl<'a> Entry<'a> {
8787
compression: None,
8888
}
8989
}
90+
91+
pub fn new_file(name: Path, data: Cow<'a, [u8]>) -> Self {
92+
Entry {
93+
name,
94+
mode: 0o644,
95+
crc: 0,
96+
data,
97+
compression: None,
98+
}
99+
}
90100
}
91101

92102
pub fn entries_to_disk<'a>(entries: &[Entry<'a>], base: &Path) -> Result<(), Error> {

packages/zpm-parsers/src/json.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::BTreeMap, ops::Range};
1+
use std::{collections::BTreeMap, ops::Range, str::FromStr};
22

33
use itertools::Itertools;
44
use serde::{Deserialize, Serialize, de::DeserializeOwned};
@@ -14,6 +14,19 @@ pub use sonic_rs as json_provider;
1414
pub type RawJsonDeserializer<R> = json_provider::Deserializer<R>;
1515
pub type RawJsonValue = json_provider::Value;
1616

17+
#[derive(Debug)]
18+
pub struct JsonSource<T> {
19+
pub value: T,
20+
}
21+
22+
impl<T: DeserializeOwned> FromStr for JsonSource<T> {
23+
type Err = Error;
24+
25+
fn from_str(s: &str) -> Result<Self, Self::Err> {
26+
Ok(Self { value: JsonDocument::hydrate_from_str(s)? })
27+
}
28+
}
29+
1730
pub struct JsonDocument {
1831
pub input: Vec<u8>,
1932
pub paths: BTreeMap<Path, usize>,

0 commit comments

Comments
 (0)