Skip to content

Commit ffb1449

Browse files
committed
feat(pm): bundle unified ecosystem packageExtensions defaults
Apply a curated set of package extensions at resolve time as the lowest-precedence layer, complementing the install-time phantom eject: declaring a package's missing dep in its manifest lets the import resolve in the global virtual store, so the package stays symlinked instead of being ejected. The bundled set is the union of @yarnpkg/extensions (159), pnpm's pnpm-specific entries (3), and high-impact phantoms nub's own scanner finds that neither covers. Vendored at vendor/package-extensions/ and kept fresh by scripts/sync-package-extensions.ts; a new emit-extensions bin converts nub-phantom scan output into the packageExtensions shape. The bundled defaults flow through a new bundled_package_extensions EngineContext field, read only by resolve_dependency_policy and never by effective_package_extensions (which feeds the lockfile packageExtensionsChecksum). Routing them through the checksum would drift every existing lockfile on each bundled-list bump and abort --frozen-lockfile. User packageExtensions win on a matching selector via extend_missing's first-write-wins; a dependency's own declared fields are never overwritten.
1 parent f06a4c9 commit ffb1449

14 files changed

Lines changed: 3125 additions & 1 deletion

File tree

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.

crates/nub-cli/src/pm_engine/mod.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,13 @@ fn apply_config_scope(
12791279
c.embedder_overrides = Some(effective);
12801280
c.trusted_dependencies_honored = trusted;
12811281
c.embedder_package_extensions = Some(effective_pe);
1282+
// Bundled ecosystem defaults (Yarn ∪ pnpm ∪ nub-phantom), applied as
1283+
// the lowest-precedence packageExtensions layer. NOT role-gated: these
1284+
// are neutral manifest-fix data, not a PM's branded config, so they
1285+
// apply under every identity (matching pnpm applying its compat
1286+
// extensions regardless of context). Kept out of the checksum by
1287+
// reading them through the separate `bundled_package_extensions` seam.
1288+
c.bundled_package_extensions = Some(bundled_package_extensions_defaults());
12821289
});
12831290

12841291
if noise == ConfigScopeNoise::Warn {
@@ -1306,6 +1313,29 @@ fn apply_config_scope(
13061313
Ok(())
13071314
}
13081315

1316+
/// Bundled ecosystem `packageExtensions` defaults (Yarn ∪ pnpm ∪
1317+
/// nub-phantom), vendored at `vendor/package-extensions/unified.json` and
1318+
/// kept fresh by `scripts/sync-package-extensions.ts`. Applied as the
1319+
/// lowest-precedence layer in aube's `resolve_dependency_policy` (user
1320+
/// extensions win per-key via `extend_missing`'s first-write-wins), and
1321+
/// deliberately excluded from the lockfile `packageExtensionsChecksum` by
1322+
/// flowing through the separate `bundled_package_extensions` seam.
1323+
///
1324+
/// A parse failure is non-fatal: the bundled file is committed and
1325+
/// compile-time-`include_str!`'d, so corruption would be a bad commit, not
1326+
/// a runtime input — warn and install with no bundled defaults rather than
1327+
/// abort an install over data the user never authored.
1328+
fn bundled_package_extensions_defaults() -> std::collections::BTreeMap<String, serde_json::Value> {
1329+
const BUNDLED: &str = include_str!("../../../../vendor/package-extensions/unified.json");
1330+
match serde_json::from_str(BUNDLED) {
1331+
Ok(map) => map,
1332+
Err(err) => {
1333+
tracing::warn!("ignoring unparseable bundled package-extensions defaults: {err}");
1334+
std::collections::BTreeMap::new()
1335+
}
1336+
}
1337+
}
1338+
13091339
/// Does the active PM honor `catalog:` specifiers? pnpm@9+, bun@1.2+, and
13101340
/// yarn-berry (v2+) implement catalogs; npm and yarn-classic (v1) do not. nub
13111341
/// identity honors catalogs (an un-branded cross-tool field, like
@@ -5104,4 +5134,55 @@ mod tests {
51045134
install state never saw"
51055135
);
51065136
}
5137+
5138+
// The bundled ecosystem defaults (Yarn ∪ pnpm ∪ nub-phantom) must load
5139+
// from the vendored `vendor/package-extensions/unified.json` and parse
5140+
// into the selector -> body map aube consumes. This guards the
5141+
// `include_str!` path and the data's correctness: the map is non-empty,
5142+
// carries the pnpm-specific `@angular/build@*` entry, and carries the
5143+
// Yarn `gatsby-core-utils@<2.14.0-next.1` entry with BOTH `got` and
5144+
// `@babel/runtime` — the latter checks the sync script's deep-merge of
5145+
// @yarnpkg/extensions' one duplicate selector (last-wins would drop
5146+
// `@babel/runtime`).
5147+
#[test]
5148+
fn bundled_package_extensions_defaults_load() {
5149+
let map = bundled_package_extensions_defaults();
5150+
assert!(
5151+
map.len() > 100,
5152+
"bundled defaults should carry 100+ entries, got {}",
5153+
map.len()
5154+
);
5155+
// pnpm-specific entry not in Yarn.
5156+
let angular = map
5157+
.get("@angular/build@*")
5158+
.expect("@angular/build@* present");
5159+
let tslib = angular
5160+
.get("dependencies")
5161+
.and_then(|d| d.get("tslib"))
5162+
.and_then(|v| v.as_str());
5163+
assert_eq!(
5164+
tslib,
5165+
Some("^2.3.0"),
5166+
"@angular/build@* -> dependencies.tslib"
5167+
);
5168+
5169+
// Yarn entry whose selector is duplicated in the source array; the
5170+
// two bodies (got, @babel/runtime) must both survive the deep-merge.
5171+
let gatsby = map
5172+
.get("gatsby-core-utils@<2.14.0-next.1")
5173+
.expect("gatsby-core-utils entry present");
5174+
let deps = gatsby
5175+
.get("dependencies")
5176+
.expect("gatsby-core-utils entry has dependencies");
5177+
assert_eq!(
5178+
deps.get("got").and_then(|v| v.as_str()),
5179+
Some("8.3.2"),
5180+
"gatsby-core-utils -> dependencies.got"
5181+
);
5182+
assert_eq!(
5183+
deps.get("@babel/runtime").and_then(|v| v.as_str()),
5184+
Some("^7.14.8"),
5185+
"gatsby-core-utils -> dependencies.@babel/runtime (survives dup-selector merge)"
5186+
);
5187+
}
51075188
}

crates/nub-cli/tests/package_extensions.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ fn store_has(dir: &Path, name: &str) -> bool {
7777
.any(|n| n.starts_with(&prefix))
7878
}
7979

80+
/// Whether the virtual store holds the exact `name@version` entry.
81+
fn store_has_version(dir: &Path, name: &str, version: &str) -> bool {
82+
let target = format!("{name}@{version}");
83+
std::fs::read_dir(dir.join("node_modules/.store"))
84+
.into_iter()
85+
.flatten()
86+
.flatten()
87+
.filter_map(|e| e.file_name().into_string().ok())
88+
.any(|n| n == target)
89+
}
90+
8091
/// A top-level `packageExtensions` entry injecting a dependency into a resolved
8192
/// package must shape the graph under Nub identity, and editing it after an
8293
/// install must invalidate the fast path so the injected dep lands.
@@ -130,3 +141,131 @@ fn top_level_package_extensions_shapes_resolution_and_invalidates_freshness() {
130141
the edit must invalidate the install fast path so it re-resolves: {err2}"
131142
);
132143
}
144+
145+
/// Read the `packageExtensionsChecksum` aube stamps onto `nub.lock` (pnpm-v9
146+
/// YAML format), or `None` when the lockfile carries no checksum.
147+
fn lockfile_checksum(dir: &Path) -> Option<String> {
148+
let lock = std::fs::read_to_string(dir.join("nub.lock")).ok()?;
149+
for line in lock.lines() {
150+
if let Some(rest) = line.trim_start().strip_prefix("packageExtensionsChecksum:") {
151+
let v = rest.trim().trim_matches('"');
152+
if !v.is_empty() {
153+
return Some(v.to_string());
154+
}
155+
}
156+
}
157+
None
158+
}
159+
160+
/// A bundled ecosystem default (Yarn ∪ pnpm ∪ nub-phantom, vendored at
161+
/// `vendor/package-extensions/unified.json`) must shape the resolved graph
162+
/// with NO user `packageExtensions` — and must NOT leak into the lockfile
163+
/// `packageExtensionsChecksum` (routing it there would drift every existing
164+
/// lockfile on each bundled-list bump and abort `--frozen-lockfile`).
165+
///
166+
/// `gatsby-core-utils@2.13.0` declares neither `got` nor `@babel/runtime`,
167+
/// and `2.13.0` satisfies the bundled selector
168+
/// `gatsby-core-utils@<2.14.0-next.1`, so the bundled extension injecting
169+
/// `got` is observable: without it, `got` is absent from the graph.
170+
///
171+
/// The checksum guard has two cases: (1) empty user `packageExtensions` →
172+
/// aube writes NO `packageExtensionsChecksum` field (the checksum fn returns
173+
/// `None` for an empty map), so a bundled-list bump cannot drift the
174+
/// lockfile — there is nothing to mismatch; (2) non-empty user
175+
/// `packageExtensions` with the bundled default ALSO shaping the graph → the
176+
/// checksum must equal `package_extensions_checksum(&user_pe_only)`, proving
177+
/// the bundled map is not folded into the checksum input.
178+
#[test]
179+
#[ignore = "network: resolves gatsby-core-utils@2.13.0 + the bundled got from the npm registry"]
180+
fn bundled_default_shapes_graph_and_stays_out_of_checksum() {
181+
use aube_lockfile::pnpm::package_extensions_checksum;
182+
if !registry_reachable() {
183+
eprintln!("skipping: registry.npmjs.org unreachable");
184+
return;
185+
}
186+
let store = pm_tmpdir("store");
187+
let cache = pm_tmpdir("cache");
188+
189+
// (1) No user packageExtensions: the bundled default must still apply,
190+
// and the lockfile must carry NO checksum (empty user PE → None → a
191+
// bundled-list bump cannot drift this lockfile).
192+
let dir_a = pm_tmpdir("bundled-a");
193+
let pkg_a =
194+
r#"{"name":"bundled-a","version":"1.0.0","dependencies":{"gatsby-core-utils":"2.13.0"}}"#;
195+
std::fs::write(dir_a.join("package.json"), pkg_a).unwrap();
196+
let (err_a, code_a) = run_install_in_store(&dir_a, &store, &cache, &["install"]);
197+
assert_eq!(code_a, 0, "bundled-default install A failed: {err_a}");
198+
assert!(
199+
store_has(&dir_a, "got"),
200+
"the bundled `gatsby-core-utils@<2.14.0-next.1` extension must inject `got` \
201+
(undeclared by 2.13.0) into the graph with no user packageExtensions: {err_a}"
202+
);
203+
assert!(
204+
dir_a.join("nub.lock").is_file(),
205+
"A: nub-identity install writes nub.lock: {err_a}"
206+
);
207+
assert_eq!(
208+
lockfile_checksum(&dir_a),
209+
None,
210+
"empty user packageExtensions must produce NO packageExtensionsChecksum \
211+
(the checksum fn returns None for an empty map), so a bundled-list bump \
212+
cannot drift the lockfile: {err_a}"
213+
);
214+
215+
// (2) Non-empty user packageExtensions, with the bundled default ALSO
216+
// shaping the graph: the checksum must reflect ONLY the user's
217+
// packageExtensions, not the bundled map. Compare against
218+
// `package_extensions_checksum` computed on the user-PE-only map.
219+
let dir_b = pm_tmpdir("bundled-b");
220+
let user_pe = r#"{"is-positive@3.1.0":{"dependencies":{"is-number":"7.0.0"}}}"#;
221+
let pkg_b = format!(
222+
r#"{{"name":"bundled-b","version":"1.0.0","dependencies":{{"gatsby-core-utils":"2.13.0","is-positive":"3.1.0"}},"packageExtensions":{user_pe}}}"#
223+
);
224+
std::fs::write(dir_b.join("package.json"), pkg_b).unwrap();
225+
let (err_b, code_b) = run_install_in_store(&dir_b, &store, &cache, &["install"]);
226+
assert_eq!(code_b, 0, "bundled-default install B failed: {err_b}");
227+
assert!(
228+
store_has(&dir_b, "got"),
229+
"B: bundled default still applies alongside user packageExtensions: {err_b}"
230+
);
231+
let user_pe_map: std::collections::BTreeMap<String, serde_json::Value> =
232+
serde_json::from_str(user_pe).unwrap();
233+
let expected =
234+
package_extensions_checksum(&user_pe_map).expect("non-empty user PE yields a checksum");
235+
assert_eq!(
236+
lockfile_checksum(&dir_b).as_deref(),
237+
Some(expected.as_str()),
238+
"the lockfile packageExtensionsChecksum must equal the hash of the \
239+
USER packageExtensions only — the bundled map (actively shaping this \
240+
graph via `got`) must not be folded into the checksum input, or every \
241+
bundled-list bump drifts existing lockfiles and aborts \
242+
--frozen-lockfile: {err_b}"
243+
);
244+
245+
// (3) User packageExtensions OVERRIDE the bundled default on a matching
246+
// selector + dependency key. The bundled `gatsby-core-utils@<2.14.0-next.1`
247+
// injects `got: 8.3.2`; a user entry for the SAME selector injecting
248+
// `got: 8.3.0` must win (user-first Vec ordering + `extend_missing`
249+
// first-write-wins), so the resolved `got` is the user's 8.3.0, not the
250+
// bundled 8.3.2. This guards the precedence construction in
251+
// `resolve_dependency_policy`, which the existing aube `extend_missing`
252+
// unit tests do not cover.
253+
let dir_c = pm_tmpdir("bundled-c");
254+
let user_pe_c = r#"{"gatsby-core-utils@<2.14.0-next.1":{"dependencies":{"got":"8.3.0"}}}"#;
255+
let pkg_c = format!(
256+
r#"{{"name":"bundled-c","version":"1.0.0","dependencies":{{"gatsby-core-utils":"2.13.0"}},"packageExtensions":{user_pe_c}}}"#
257+
);
258+
std::fs::write(dir_c.join("package.json"), pkg_c).unwrap();
259+
let (err_c, code_c) = run_install_in_store(&dir_c, &store, &cache, &["install"]);
260+
assert_eq!(code_c, 0, "bundled-default install C failed: {err_c}");
261+
assert!(
262+
store_has_version(&dir_c, "got", "8.3.0"),
263+
"user packageExtensions must override the bundled `got: 8.3.2` with the \
264+
user's `got: 8.3.0` on the same selector: {err_c}"
265+
);
266+
assert!(
267+
!store_has_version(&dir_c, "got", "8.3.2"),
268+
"the bundled `got: 8.3.2` must NOT be resolved when the user overrides \
269+
the same selector+key with 8.3.0: {err_c}"
270+
);
271+
}

crates/nub-phantom-scan/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ path = "src/lib.rs"
2424
nub-phantom-core = { path = "../nub-phantom-core" }
2525
serde = { version = "1", features = ["derive"] }
2626
serde_json = "1"
27+
# Used by the `emit-extensions` bin's dedup matcher (selector → name+range
28+
# against a scanned version), mirroring aube-resolver's `package_selector_matches`.
29+
node-semver = "2"
2730

2831
[lints]
2932
workspace = true

0 commit comments

Comments
 (0)