Skip to content

Commit c3a651d

Browse files
authored
Merge branch 'main' into master
2 parents 688a9a5 + 34c36aa commit c3a651d

4 files changed

Lines changed: 185 additions & 37 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defaults:
2424

2525
jobs:
2626
detect-changes:
27-
runs-on: ubuntu-latest
27+
runs-on: namespace-profile-linux-x64-default
2828
permissions:
2929
contents: read
3030
pull-requests: read
@@ -43,7 +43,7 @@ jobs:
4343
needs: detect-changes
4444
if: needs.detect-changes.outputs.code-changed == 'true'
4545
name: Clippy
46-
runs-on: ubuntu-latest
46+
runs-on: namespace-profile-linux-x64-default
4747
steps:
4848
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
4949
with:
@@ -57,7 +57,10 @@ jobs:
5757
components: clippy
5858

5959
- run: rustup target add x86_64-unknown-linux-musl
60-
- run: pip install cargo-zigbuild
60+
- run: pipx install cargo-zigbuild
61+
# pipx isolates cargo-zigbuild in its own venv, so its ziglang dependency
62+
# (which bundles zig) isn't on PATH. Install zig separately.
63+
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
6164

6265
# --locked: verify Cargo.lock is up to date (replaces the removed `cargo check --locked`)
6366
- run: cargo clippy --locked --all-targets --all-features -- -D warnings
@@ -70,14 +73,22 @@ jobs:
7073
fail-fast: false
7174
matrix:
7275
include:
73-
- os: ubuntu-latest
76+
- os: namespace-profile-linux-x64-default
7477
target: x86_64-unknown-linux-gnu
78+
cargo_cmd: cargo-zigbuild
79+
build_target: x86_64-unknown-linux-gnu.2.17
7580
- os: windows-latest
7681
target: x86_64-pc-windows-msvc
77-
- os: macos-latest
82+
cargo_cmd: cargo
83+
build_target: x86_64-pc-windows-msvc
84+
- os: namespace-profile-mac-default
7885
target: aarch64-apple-darwin
79-
- os: macos-latest
86+
cargo_cmd: cargo
87+
build_target: aarch64-apple-darwin
88+
- os: namespace-profile-mac-default
8089
target: x86_64-apple-darwin
90+
cargo_cmd: cargo
91+
build_target: x86_64-apple-darwin
8192
runs-on: ${{ matrix.os }}
8293
steps:
8394
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -102,10 +113,15 @@ jobs:
102113
- run: rustup target add ${{ matrix.target }}
103114

104115
- run: rustup target add x86_64-unknown-linux-musl
105-
if: ${{ matrix.os == 'ubuntu-latest' }}
116+
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
106117

107-
- run: pip install cargo-zigbuild
108-
if: ${{ matrix.os == 'ubuntu-latest' }}
118+
- run: pipx install cargo-zigbuild
119+
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
120+
121+
# pipx isolates cargo-zigbuild in its own venv, so its ziglang dependency
122+
# (which bundles zig) isn't on PATH. Install zig separately.
123+
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
124+
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
109125

110126
# For x86_64-apple-darwin on arm64 runner, install x64 node so fspy preload dylib
111127
# (compiled for x86_64) can be injected into node processes running under Rosetta.
@@ -128,26 +144,16 @@ jobs:
128144
- run: pnpm install
129145

130146
- name: Build tests
131-
run: cargo test --no-run --target ${{ matrix.target }}
132-
if: ${{ matrix.os != 'ubuntu-latest' }}
133-
134-
- name: Build tests
135-
run: cargo-zigbuild test --no-run --target x86_64-unknown-linux-gnu.2.17
136-
if: ${{ matrix.os == 'ubuntu-latest' }}
137-
138-
- name: Run tests
139-
run: cargo test --target ${{ matrix.target }}
140-
if: ${{ matrix.os != 'ubuntu-latest' }}
147+
run: ${{ matrix.cargo_cmd }} test --no-run --target ${{ matrix.build_target }}
141148

142149
- name: Run tests
143-
run: cargo-zigbuild test --target x86_64-unknown-linux-gnu.2.17
144-
if: ${{ matrix.os == 'ubuntu-latest' }}
150+
run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }}
145151

146152
test-musl:
147153
needs: detect-changes
148154
if: needs.detect-changes.outputs.code-changed == 'true'
149155
name: Test (musl)
150-
runs-on: ubuntu-latest
156+
runs-on: namespace-profile-linux-x64-default
151157
container:
152158
image: node:22-alpine3.21
153159
options: --shm-size=256m # shm_io tests need bigger shared memory
@@ -193,7 +199,7 @@ jobs:
193199

194200
fmt:
195201
name: Format and Check Deps
196-
runs-on: ubuntu-latest
202+
runs-on: namespace-profile-linux-x64-default
197203
steps:
198204
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
199205
with:
@@ -221,7 +227,7 @@ jobs:
221227
run: pnpm dedupe --check
222228

223229
done:
224-
runs-on: ubuntu-latest
230+
runs-on: namespace-profile-linux-x64-default
225231
if: always()
226232
needs:
227233
- clippy

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ vp run -t @my/app#build # run in a package and its transitive dependencies
1313
vp run --cache build # run with caching enabled
1414
```
1515

16+
## Sponsors
17+
18+
Thanks to [namespace.so](https://namespace.so) for powering our CI/CD pipelines with fast, free macOS and Linux runners.
19+
1620
## License
1721

1822
[MIT](LICENSE)

crates/pty_terminal/tests/terminal.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ fn write_interactive_prompt() {
140140
let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| {
141141
use std::io::{Write, stdin, stdout};
142142
let mut stdout = stdout();
143-
print!("Name: ");
143+
// Use "Name:\n" instead of "Name: " so the test can synchronize with
144+
// `read_until(b'\n')`. On Windows ConPTY, `read_until(b' ')` on the
145+
// raw PTY stream can hang.
146+
println!("Name:");
144147
stdout.flush().unwrap();
145148

146149
let mut input = std::string::String::new();
@@ -152,12 +155,12 @@ fn write_interactive_prompt() {
152155
let Terminal { mut pty_reader, mut pty_writer, child_handle, .. } =
153156
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
154157

155-
// Wait for prompt "Name: " (read until the space after colon)
158+
// Wait for the "Name:" prompt line.
156159
{
157160
let mut buf_reader = BufReader::new(&mut pty_reader);
158-
let mut buf = Vec::new();
159-
buf_reader.read_until(b' ', &mut buf).unwrap();
160-
assert!(String::from_utf8_lossy(&buf).contains("Name:"));
161+
let mut line = Vec::new();
162+
buf_reader.read_until(b'\n', &mut line).unwrap();
163+
assert!(String::from_utf8_lossy(&line).contains("Name:"));
161164
}
162165

163166
// Send response
@@ -168,7 +171,7 @@ fn write_interactive_prompt() {
168171
let _ = child_handle.wait().unwrap();
169172

170173
let output = pty_reader.screen_contents();
171-
assert_eq!(output.trim(), "Name: Alice\nHello, Alice");
174+
assert_eq!(output.trim(), "Name:\nAlice\nHello, Alice");
172175
}
173176

174177
#[test]

crates/vite_workspace/src/lib.rs

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,41 @@ struct PnpmWorkspace {
3333
packages: Vec<Str>,
3434
}
3535

36-
/// The workspace configuration for npm/yarn.
36+
/// The `workspaces` field in package.json can be either an array of glob patterns
37+
/// or an object with a `packages` field (used by Bun catalogs and Yarn classic nohoist).
38+
///
39+
/// Array form: `"workspaces": ["packages/*", "apps/*"]`
40+
/// Object form: `"workspaces": {"packages": ["packages/*", "apps/*"], "catalog": {...}}`
41+
///
42+
/// Bun: <https://bun.sh/docs/pm/workspaces>
43+
/// Yarn classic: <https://classic.yarnpkg.com/en/docs/workspaces/>
44+
#[derive(Debug, Deserialize)]
45+
#[serde(untagged)]
46+
enum NpmWorkspaces {
47+
/// Array of glob patterns (npm, yarn, bun).
48+
Array(Vec<Str>),
49+
/// Object form with a `packages` field (Bun catalogs, Yarn classic nohoist).
50+
Object { packages: Vec<Str> },
51+
}
52+
53+
impl NpmWorkspaces {
54+
fn into_packages(self) -> Vec<Str> {
55+
match self {
56+
Self::Array(packages) | Self::Object { packages } => packages,
57+
}
58+
}
59+
}
60+
61+
/// The workspace configuration for npm/yarn/bun.
3762
///
3863
/// npm: <https://docs.npmjs.com/cli/v11/using-npm/workspaces>
3964
/// yarn: <https://yarnpkg.com/features/workspaces>
65+
/// bun: <https://bun.sh/docs/pm/workspaces>
4066
#[derive(Debug, Deserialize)]
4167
struct NpmWorkspace {
42-
/// Array of folder glob patterns referencing the workspaces of the project.
43-
///
44-
/// <https://docs.npmjs.com/cli/v11/configuring-npm/package-json#workspaces>
45-
/// <https://yarnpkg.com/configuration/manifest#workspaces>
46-
workspaces: Vec<Str>,
68+
/// Glob patterns referencing the workspaces of the project.
69+
/// Accepts both array form and object form (with `packages` key).
70+
workspaces: NpmWorkspaces,
4771
}
4872

4973
#[derive(Debug)]
@@ -237,7 +261,7 @@ pub fn load_package_graph(
237261
file_path: Arc::clone(file_with_path.path()),
238262
serde_json_error: e,
239263
})?;
240-
workspace.workspaces
264+
workspace.workspaces.into_packages()
241265
}
242266
WorkspaceFile::NonWorkspacePackage(file_with_path) => {
243267
// For non-workspace packages, add the package.json to the graph as a root package
@@ -1096,4 +1120,115 @@ mod tests {
10961120
// External dependencies should not create edges
10971121
assert_eq!(graph.edge_count(), 1, "Should only have one edge for workspace dependency");
10981122
}
1123+
1124+
#[test]
1125+
fn test_get_package_graph_npm_workspace_object_form() {
1126+
let temp_dir = TempDir::new().unwrap();
1127+
let temp_dir_path = AbsolutePath::new(temp_dir.path()).unwrap();
1128+
1129+
// Create package.json with object-form workspaces (Bun/Yarn classic style)
1130+
let root_package = serde_json::json!({
1131+
"name": "bun-monorepo",
1132+
"private": true,
1133+
"workspaces": {
1134+
"packages": ["packages/*", "apps/*"]
1135+
}
1136+
});
1137+
fs::write(temp_dir_path.join("package.json"), root_package.to_string()).unwrap();
1138+
1139+
// Create packages directory structure
1140+
fs::create_dir_all(temp_dir_path.join("packages")).unwrap();
1141+
fs::create_dir_all(temp_dir_path.join("apps")).unwrap();
1142+
1143+
// Create shared library package
1144+
fs::create_dir_all(temp_dir_path.join("packages/shared")).unwrap();
1145+
let shared_pkg = serde_json::json!({
1146+
"name": "@myorg/shared",
1147+
"version": "1.0.0"
1148+
});
1149+
fs::write(temp_dir_path.join("packages/shared/package.json"), shared_pkg.to_string())
1150+
.unwrap();
1151+
1152+
// Create app that depends on shared
1153+
fs::create_dir_all(temp_dir_path.join("apps/web")).unwrap();
1154+
let web_app = serde_json::json!({
1155+
"name": "web-app",
1156+
"version": "0.1.0",
1157+
"dependencies": {
1158+
"@myorg/shared": "workspace:*"
1159+
}
1160+
});
1161+
fs::write(temp_dir_path.join("apps/web/package.json"), web_app.to_string()).unwrap();
1162+
1163+
let graph = discover_package_graph(temp_dir_path).unwrap();
1164+
1165+
// Should have 3 nodes: root + shared + web-app
1166+
assert_eq!(graph.node_count(), 3);
1167+
1168+
// Verify packages were found
1169+
let mut packages_found = FxHashSet::<Str>::default();
1170+
for node in graph.node_weights() {
1171+
packages_found.insert(node.package_json.name.clone());
1172+
}
1173+
assert!(packages_found.contains("bun-monorepo"));
1174+
assert!(packages_found.contains("@myorg/shared"));
1175+
assert!(packages_found.contains("web-app"));
1176+
1177+
// Verify dependency edge
1178+
let mut found_web_to_shared = false;
1179+
for edge_ref in graph.edge_references() {
1180+
let source = &graph[edge_ref.source()];
1181+
let target = &graph[edge_ref.target()];
1182+
if source.package_json.name == "web-app" && target.package_json.name == "@myorg/shared"
1183+
{
1184+
found_web_to_shared = true;
1185+
}
1186+
}
1187+
assert!(found_web_to_shared, "Web app should depend on shared");
1188+
}
1189+
1190+
#[test]
1191+
fn test_get_package_graph_bun_workspace_with_catalog() {
1192+
let temp_dir = TempDir::new().unwrap();
1193+
let temp_dir_path = AbsolutePath::new(temp_dir.path()).unwrap();
1194+
1195+
// Create package.json with Bun catalog in object-form workspaces
1196+
let root_package = serde_json::json!({
1197+
"name": "bun-catalog-monorepo",
1198+
"private": true,
1199+
"workspaces": {
1200+
"packages": ["packages/*"],
1201+
"catalog": {
1202+
"react": "^19.0.0",
1203+
"vite": "npm:@voidzero-dev/vite-plus-core@latest"
1204+
}
1205+
}
1206+
});
1207+
fs::write(temp_dir_path.join("package.json"), root_package.to_string()).unwrap();
1208+
1209+
// Create packages directory
1210+
fs::create_dir_all(temp_dir_path.join("packages")).unwrap();
1211+
1212+
// Create a package
1213+
fs::create_dir_all(temp_dir_path.join("packages/app")).unwrap();
1214+
let app_pkg = serde_json::json!({
1215+
"name": "my-app",
1216+
"dependencies": {
1217+
"react": "catalog:"
1218+
}
1219+
});
1220+
fs::write(temp_dir_path.join("packages/app/package.json"), app_pkg.to_string()).unwrap();
1221+
1222+
let graph = discover_package_graph(temp_dir_path).unwrap();
1223+
1224+
// Should have 2 nodes: root + app (catalog field is silently ignored)
1225+
assert_eq!(graph.node_count(), 2);
1226+
1227+
let mut packages_found = FxHashSet::<Str>::default();
1228+
for node in graph.node_weights() {
1229+
packages_found.insert(node.package_json.name.clone());
1230+
}
1231+
assert!(packages_found.contains("bun-catalog-monorepo"));
1232+
assert!(packages_found.contains("my-app"));
1233+
}
10991234
}

0 commit comments

Comments
 (0)