Skip to content
4 changes: 2 additions & 2 deletions src/install/PackageManager/package_json_write_back.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::dependency::DependencyExt as _;
use crate::lockfile::package::PackageColumns as _;
use crate::lockfile::{Lockfile, Package};
use crate::resolution::Tag as ResolutionTag;
use crate::{Dependency, PackageID, PackageNameHash, invalid_package_id};
use crate::{Dependency, Features, PackageID, PackageNameHash, invalid_package_id};

use super::add_catalog;
use super::add_remove_with_filter::{
Expand Down Expand Up @@ -267,7 +267,7 @@ fn target_package_ids(lockfile: &Lockfile, edited: &[EditedPackageJson]) -> Vec<
/// Re-parses the edited files the way `bun install` would and copies every declared literal that differs (and, for the root, `overrides` + `catalogs`) into `manager.lockfile`, so the next install's differ sees no change.
fn sync_lockfile(manager: &mut PackageManager, edited: &[EditedPackageJson]) -> crate::Result<()> {
let mut scratch = super::workspace_manifests::ScratchManifests::new();
scratch.parse_root(manager)?;
scratch.parse_root(manager, Features::main())?;
let mut root_pkg = Some(core::mem::take(&mut scratch.root));
let mut parsed: Vec<(usize, Package)> = Vec::with_capacity(edited.len());
for (i, e) in edited.iter().enumerate() {
Expand Down
109 changes: 98 additions & 11 deletions src/install/PackageManager/workspace_manifests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use core::fmt;

use bstr::BStr;
use bun_collections::HashMap;
use bun_core::{Global, Output};
Expand All @@ -11,6 +13,7 @@ use super::add_remove_with_filter::{WorkspaceTarget, fetch_entry, root_package_j
use super::workspace_selection::WorkspaceGraph;

/// Root + member package.json files parsed the way `bun install` parses them, into a throw-away lockfile.
/// Errors the parse only logs (an invalid catalog range, say) fail it here, as they fail `bun install`.
pub(crate) struct ScratchManifests {
pub(crate) lockfile: Lockfile,
pub(crate) log: bun_ast::Log,
Expand All @@ -27,7 +30,12 @@ impl ScratchManifests {
}

/// Must run first: it fills `lockfile.workspace_paths`, which `workspace:` rows in every file resolve through.
pub(crate) fn parse_root(&mut self, manager: &mut PackageManager) -> crate::Result<()> {
/// `features` must include `workspaces` for that, and `is_main` for the catalogs.
pub(crate) fn parse_root(
&mut self,
manager: &mut PackageManager,
features: Features,
) -> crate::Result<()> {
let root_target = WorkspaceTarget {
name: Box::default(),
name_hash: None,
Expand All @@ -46,8 +54,9 @@ impl ScratchManifests {
&root_source,
root_json,
&mut resolver,
Features::main(),
)
features,
)?;
self.fail_on_logged_errors()
}

pub(crate) fn parse_member(
Expand All @@ -71,8 +80,77 @@ impl ScratchManifests {
&mut resolver,
Features::WORKSPACE,
)?;
self.fail_on_logged_errors()?;
Ok(pkg)
}

fn fail_on_logged_errors(&self) -> crate::Result<()> {
if self.log.has_errors() {
return Err(crate::Error::InstallFailed);
}
Ok(())
}
}

/// The workspace versions and catalogs `bun pm pack` / `bun publish` substitute, read from the
/// package.json files as they are now. Not from bun.lock: it has the versions of the last install,
/// and releases bump versions between that install and the publish.
Comment thread
robobun marked this conversation as resolved.
pub struct WorkspaceManifests {
lockfile: Lockfile,
root_package_json_path: Box<[u8]>,
}

impl WorkspaceManifests {
/// Exits with `bun install`'s errors when the root package.json or a workspace does not parse.
pub fn load(manager: &mut PackageManager) -> WorkspaceManifests {
// Only the two things pack reads: the `workspaces` walk and the catalogs. The root's own
// dependency sections are not parsed, so `bun install`'s checks on them (a `workspace:1.2.3`
// range no workspace satisfies, say) do not decide whether a package packs.
Comment thread
robobun marked this conversation as resolved.
let features = Features {
is_main: true,
workspaces: true,
dependencies: false,
peer_dependencies: false,
..Features::default()
};
let mut scratch = ScratchManifests::new();
if let Err(err) = scratch.parse_root(manager, features) {
crash(
&mut scratch.log,
err,
format_args!("failed to read the workspace's package.json files"),
);
}
WorkspaceManifests {
lockfile: scratch.lockfile,
root_package_json_path: root_package_json_path(),
}
}
Comment thread
claude[bot] marked this conversation as resolved.

/// The package.json whose `workspaces` and catalogs these are: the workspace root's when the
/// package being packed is one of its workspaces, otherwise the package's own.
Comment thread
robobun marked this conversation as resolved.
pub fn root_package_json_path(&self) -> &[u8] {
&self.root_package_json_path
}

/// The `version` in the package.json of the workspace named `name`. `None` when no workspace
/// has that name or its package.json has no (semver) version.
Comment thread
robobun marked this conversation as resolved.
pub fn workspace_version(&self, name: &[u8]) -> Option<impl fmt::Display + '_> {
let name_hash: PackageNameHash = bun_semver::string::Builder::string_hash(name);
let version = self.lockfile.workspace_versions.get(&name_hash)?;
Some(version.fmt(self.lockfile.buffers.string_bytes.as_slice()))
}

/// The range catalog `catalog_name` (`""` and `"default"` both name the default catalog)
/// declares for `dependency_name`, as written in the root package.json.
Comment thread
robobun marked this conversation as resolved.
pub fn catalog_version(&self, catalog_name: &[u8], dependency_name: &[u8]) -> Option<&[u8]> {
let string_buf = self.lockfile.buffers.string_bytes.as_slice();
let dependency = self
.lockfile
.catalogs
.find(string_buf, catalog_name, dependency_name)?;
Some(dependency.version.literal.slice(string_buf))
}
}

/// Graph index i == `targets[i]`; the target whose `name_hash` is `None` is the root.
Expand All @@ -82,8 +160,8 @@ pub(crate) fn relation_graph(
pattern: &[u8],
) -> WorkspaceGraph {
let mut scratch = ScratchManifests::new();
if let Err(err) = scratch.parse_root(manager) {
crash(&mut scratch.log, pattern, err);
if let Err(err) = scratch.parse_root(manager, Features::main()) {
crash_for_filter(&mut scratch.log, pattern, err);
}

let mut parsed: Vec<(u32, Package)> = Vec::with_capacity(targets.len());
Expand All @@ -94,7 +172,7 @@ pub(crate) fn relation_graph(
}
match scratch.parse_member(manager, target) {
Ok(pkg) => parsed.push((i as u32, pkg)),
Err(err) => crash(&mut scratch.log, pattern, err),
Err(err) => crash_for_filter(&mut scratch.log, pattern, err),
}
}

Expand Down Expand Up @@ -150,14 +228,23 @@ pub(crate) fn relation_graph(
WorkspaceGraph::from_edges(targets.len(), edges)
}

fn crash(log: &mut bun_ast::Log, pattern: &[u8], err: crate::Error) -> ! {
fn crash_for_filter(log: &mut bun_ast::Log, pattern: &[u8], err: crate::Error) -> ! {
crash(
log,
err,
format_args!(
"failed to read the workspace dependencies for --filter \"{}\"",
BStr::new(pattern)
),
)
}

/// The parse errors explain the failure when there are any; `what` and `err` are the fallback.
fn crash(log: &mut bun_ast::Log, err: crate::Error, what: fmt::Arguments<'_>) -> ! {
if log.has_errors() {
let _ = log.print(std::ptr::from_mut(Output::error_writer()));
} else {
Output::err_generic(
"failed to read the workspace dependencies for --filter \"{}\": {}",
(BStr::new(pattern), err.name()),
);
Output::err_generic("{}: {}", (what, err.name()));
}
Global::crash();
}
Loading
Loading