bun_install 0.1.0

A Rust-native programmable browser runtime built on Servo and SpiderMonkey
use crate::lockfile::package::PackageColumns as _;
use core::cmp::Ordering;

use bun_collections::HashMap;
use bun_core::strings;
use bun_semver::String as SemverString;

use crate::lockfile_real::package::Alphabetizer;
use bun_install::Dependency;
use bun_install::PackageID;
use bun_install::Resolution;
use bun_install::dependency::{self, Behavior, VersionExt as _};
use bun_install::lockfile::package;
// PORT NOTE: `lockfile.packages.slice()` returns
// `bun_collections::multi_array_list::Slice<Package<_>>`; the `items_<field>()`
// column accessors are an extension trait (Zig's `slice.items(.field)` is
// comptime-dispatched, Rust models it as a hand-expanded trait per Package.rs).
use crate::integrity;
use crate::lockfile_real::Printer;

// TODO(port): narrow error set (only writer + alloc errors are produced)
pub fn print(this: &mut Printer, writer: &mut impl bun_io::Write) -> Result<(), bun_core::Error> {
    // internal for debugging, print the lockfile as custom json
    // limited to debug because we don't want people to rely on this format.
    #[cfg(debug_assertions)]
    {
        // TODO(port): std.process.hasEnvVarConstant("JSON") + std.json.Stringify
        // have no direct equivalent here; wire up bun_core::env_var + a JSON
        // serializer for Lockfile if this debug path is still wanted.
        let _ = &this.lockfile;
    }

    writer.write_all(
        b"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n\
          # yarn lockfile v1\n\
          # bun ./bun.lockb --hash:",
    )?;
    write!(writer, " {}\n\n", this.lockfile.fmt_meta_hash())?;

    packages(this, writer)
}

fn packages(this: &mut Printer, writer: &mut impl bun_io::Write) -> Result<(), bun_core::Error> {
    let slice = this.lockfile.packages.slice();
    let names: &[SemverString] = slice.items_name();
    let resolved: &[Resolution] = slice.items_resolution();
    let metas: &[package::Meta] = slice.items_meta();
    if names.is_empty() {
        return Ok(());
    }
    let dependency_lists = slice.items_dependencies();
    let resolutions_buffer: &[PackageID] = this.lockfile.buffers.resolutions.as_slice();
    let dependencies_buffer: &[Dependency] = this.lockfile.buffers.dependencies.as_slice();

    // Zig: std.HashMap(PackageID, []Dependency.Version, IdentityContext(PackageID), 80)
    // PORT NOTE: reshaped for borrowck — store (start, len) into
    // `all_requested_versions_buf` instead of overlapping &mut [Version] slices.
    let mut requested_versions: HashMap<PackageID, (usize, usize)> = HashMap::default();

    // PERF(port): Zig was raw `allocator.alloc(Dependency.Version, resolutions_buffer.len)` of
    // uninit memory + cursor slicing. We push into a pre-reserved Vec instead — set_len would
    // drop uninit tail elements (and index-assign would drop uninit old values). Profile if hot.
    let mut all_requested_versions_buf: Vec<dependency::Version> =
        Vec::with_capacity(resolutions_buffer.len());

    let package_count = names.len() as PackageID;
    let mut alphabetized_names: Vec<PackageID> = vec![0; (package_count - 1) as usize];

    let string_buf: &[u8] = this.lockfile.buffers.string_bytes.as_slice();

    // First, we need to build a map of all requested versions
    // This is so we can print requested versions
    {
        let mut i: PackageID = 1;
        while i < package_count {
            alphabetized_names[(i - 1) as usize] = i;

            let mut resolutions = resolutions_buffer;
            let mut dependencies = dependencies_buffer;

            let mut j: usize = 0;
            let requested_version_start = all_requested_versions_buf.len();
            while let Some(k) = resolutions.iter().position(|&r| r == i) {
                j += 1;

                all_requested_versions_buf.push(dependencies[k].version.clone());

                dependencies = &dependencies[k + 1..];
                resolutions = &resolutions[k + 1..];
            }

            let dependency_versions = &mut all_requested_versions_buf[requested_version_start..];
            if dependency_versions.len() > 1 {
                // PERF(port): was std.sort.insertion — profile if it shows up on a hot path.
                dependency_versions.sort_by(|a, b| {
                    if dependency::Version::is_less_than_with_tag(string_buf, a, b) {
                        Ordering::Less
                    } else if dependency::Version::is_less_than_with_tag(string_buf, b, a) {
                        Ordering::Greater
                    } else {
                        Ordering::Equal
                    }
                });
            }
            requested_versions.insert(i, (requested_version_start, j));

            i += 1;
        }
    }

    {
        let alphabetizer = Alphabetizer::<u64> {
            names: names.into(),
            buf: string_buf.into(),
            resolutions: resolved.into(),
        };
        // PERF(port): std.sort.pdq → sort_unstable_by (Rust uses pdqsort internally)
        alphabetized_names.sort_unstable_by(|&a, &b| alphabetizer.order(a, b));
    }

    // When printing, we start at 1
    for &i in alphabetized_names.iter() {
        let name: &[u8] = names[i as usize].slice(string_buf);
        let resolution = &resolved[i as usize];
        let meta = &metas[i as usize];
        let dependencies: &[Dependency] = dependency_lists[i as usize].get(dependencies_buffer);
        let version_formatter = resolution.fmt(string_buf, bun_core::fmt::PathSep::Posix);

        // This prints:
        // "@babel/core@7.9.0":
        {
            writer.write_all(b"\n")?;
            let (rv_start, rv_len) = *requested_versions.get(&i).unwrap();
            let dependency_versions = &all_requested_versions_buf[rv_start..rv_start + rv_len];

            // https://github.com/yarnpkg/yarn/blob/158d96dce95313d9a00218302631cd263877d164/src/lockfile/stringify.js#L9
            let always_needs_quote = strings::must_escape_yaml_string(name);

            let mut prev_dependency_version: Option<&dependency::Version> = None;
            let mut needs_comma = false;
            for dependency_version in dependency_versions {
                if needs_comma {
                    if let Some(prev) = prev_dependency_version {
                        if prev.eql(dependency_version, string_buf, string_buf) {
                            continue;
                        }
                    }
                    writer.write_all(b", ")?;
                }
                let version_name: &[u8] = dependency_version.literal.slice(string_buf);
                let needs_quote = always_needs_quote
                    || strings::index_of_any(version_name, b" |\t-/!:\"\\,\n\r").is_some()
                    || version_name.starts_with(b"npm:");

                if needs_quote {
                    writer.write_all(b"\"")?;
                    write!(
                        writer,
                        "{}",
                        bun_core::fmt::format_json_string_utf8(
                            name,
                            bun_core::fmt::JSONFormatterUTF8Options { quote: false }
                        ),
                    )?;
                } else {
                    writer.write_all(name)?;
                }
                writer.write_all(b"@")?;
                if version_name.is_empty() {
                    writer.write_all(b"^")?;
                    version_formatter.write_to(writer)?;
                } else if needs_quote {
                    write!(
                        writer,
                        "{}",
                        bun_core::fmt::format_json_string_utf8(
                            version_name,
                            bun_core::fmt::JSONFormatterUTF8Options { quote: false }
                        ),
                    )?;
                } else {
                    writer.write_all(version_name)?;
                }

                if needs_quote {
                    writer.write_all(b"\"")?;
                }
                prev_dependency_version = Some(dependency_version);
                needs_comma = true;
            }

            writer.write_all(b":\n")?;
        }

        {
            let mut quoted_buf: Vec<u8> = Vec::new();

            writer.write_all(b"  version ")?;

            // Version is always quoted
            version_formatter.write_to(&mut quoted_buf)?;
            writeln!(
                writer,
                "{}",
                bun_core::fmt::format_json_string_utf8(&quoted_buf, Default::default()),
            )?;

            writer.write_all(b"  resolved ")?;

            let url_formatter = resolution.fmt_url(string_buf);

            // Resolved URL is always quoted
            quoted_buf.clear();
            url_formatter.write_to(&mut quoted_buf)?;
            writeln!(
                writer,
                "{}",
                bun_core::fmt::format_json_string_utf8(&quoted_buf, Default::default()),
            )?;

            if meta.integrity.tag != integrity::Tag::UNKNOWN {
                // Integrity is...never quoted?
                writeln!(writer, "  integrity {}", meta.integrity)?;
            }

            if !dependencies.is_empty() {
                let mut behavior = Behavior::default();
                let mut dependency_behavior_change_count: u8 = 0;
                for dep in dependencies {
                    if !dep.behavior.eq(&behavior) {
                        if dep.behavior.is_optional() {
                            writer.write_all(b"  optionalDependencies:\n")?;
                            if cfg!(debug_assertions) {
                                dependency_behavior_change_count += 1;
                            }
                        } else if dep.behavior.is_prod() {
                            writer.write_all(b"  dependencies:\n")?;
                            if cfg!(debug_assertions) {
                                dependency_behavior_change_count += 1;
                            }
                        } else if dep.behavior.is_dev() {
                            writer.write_all(b"  devDependencies:\n")?;
                            if cfg!(debug_assertions) {
                                dependency_behavior_change_count += 1;
                            }
                        } else {
                            continue;
                        }
                        behavior = dep.behavior;

                        // assert its sorted. debug only because of a bug saving incorrect ordering
                        // of optional dependencies to lockfiles
                        if cfg!(debug_assertions) {
                            debug_assert!(dependency_behavior_change_count < 3);
                        }
                    }

                    writer.write_all(b"    ")?;
                    let dependency_name: &[u8] = dep.name.slice(string_buf);

                    let needs_quote = strings::must_escape_yaml_string(dependency_name);

                    if needs_quote {
                        write!(
                            writer,
                            "{}",
                            bun_core::fmt::format_json_string_utf8(
                                dependency_name,
                                Default::default()
                            ),
                        )?;
                    } else {
                        writer.write_all(dependency_name)?;
                    }
                    writeln!(
                        writer,
                        " {}",
                        bun_core::fmt::format_json_string_utf8(
                            dep.version.literal.slice(string_buf),
                            Default::default()
                        ),
                    )?;
                }
                let _ = dependency_behavior_change_count;
            }
        }
    }

    Ok(())
}

// ported from: src/install/lockfile/printer/Yarn.zig