Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,18 @@ in the `package.json`.
> `package.json`, and it is up to the developer to ensure that results in the
> desired package manager actually running.

If the `packageManager` property is not present, then the fallback manager will
be used; this defaults to the value of the `PACKAGE_JSON_FALLBACK_MANAGER`
environment variable or otherwise `npm`. You can also provide a specific
fallback manager:
If the `packageManager` property is not present, the gem will automatically
detect which package manager to use by checking for lockfiles in this priority
order:

1. `bun.lockb` - Bun
2. `pnpm-lock.yaml` - pnpm
3. `yarn.lock` - Yarn (Berry or Classic, determined by file format)
4. `package-lock.json` - npm

If no lockfile is found, then the fallback manager will be used; this defaults
to the value of the `PACKAGE_JSON_FALLBACK_MANAGER` environment variable or
otherwise `npm`. You can also provide a specific fallback manager:

```ruby
PackageJson.read(fallback_manager: :pnpm)
Expand Down
48 changes: 47 additions & 1 deletion lib/package_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,61 @@ def record_package_manager!
def determine_package_manager(fallback_manager)
package_manager = fetch("packageManager", nil)

return fallback_manager if package_manager.nil?
return parse_package_manager(package_manager) unless package_manager.nil?

# If no packageManager property, check for lockfiles
lockfile_manager = detect_manager_from_lockfile

return lockfile_manager unless lockfile_manager.nil?

# Fall back to the provided fallback manager
fallback_manager
end

def parse_package_manager(package_manager)
name, version = package_manager.split("@")

return determine_yarn_version(version) if name == "yarn"

name.to_sym
end

def detect_manager_from_lockfile
# Check for lockfiles in priority order
# bun.lockb - Bun
return :bun if File.exist?("#{directory}/bun.lockb")

# pnpm-lock.yaml - pnpm
return :pnpm if File.exist?("#{directory}/pnpm-lock.yaml")

# yarn.lock - Yarn (need to distinguish between Berry and Classic)
if File.exist?("#{directory}/yarn.lock")
return detect_yarn_version_from_lockfile
end

# package-lock.json - npm
return :npm if File.exist?("#{directory}/package-lock.json")

# No lockfile found
nil
end

def detect_yarn_version_from_lockfile
lockfile_path = "#{directory}/yarn.lock"
return nil unless File.exist?(lockfile_path)

# Read the first few lines to determine the version
# Yarn Berry lockfiles start with "__metadata:" or have "# yarn lockfile v1" but use a different format
# Yarn Classic lockfiles start with "# THIS IS AN AUTOGENERATED FILE" and "# yarn lockfile v1"
content = File.read(lockfile_path, 1000) # Read first 1000 chars

# Yarn Berry uses __metadata: at the start
return :yarn_berry if content.include?("__metadata:")

# Default to Yarn Classic for older format
:yarn_classic
end

def determine_yarn_version(version)
raise Error, "a major version must be present for Yarn" if version.nil? || version.empty?

Expand Down
126 changes: 124 additions & 2 deletions spec/package_json_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,91 @@
end
end

it "uses the fallback manager" do
it "uses the fallback manager when no lockfile is present" do
with_package_json_file({ "version" => "1.0.0" }) do
package_json = described_class.read(Dir.pwd, fallback_manager: :yarn_classic)

expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
end
end

it "detects npm from package-lock.json" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("package-lock.json", "{}")
package_json = described_class.read(Dir.pwd, fallback_manager: :yarn_classic)

expect(package_json.manager).to be_a PackageJson::Managers::NpmLike
end
end

it "detects pnpm from pnpm-lock.yaml" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("pnpm-lock.yaml", "lockfileVersion: '6.0'")
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::PnpmLike
end
end

it "detects bun from bun.lockb" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("bun.lockb", "")
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::BunLike
end
end

it "detects yarn classic from yarn.lock without __metadata:" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("yarn.lock", "# yarn lockfile v1\n\npackage@^1.0.0:\n version \"1.0.0\"")
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
end
end

it "detects yarn berry from yarn.lock with __metadata:" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("yarn.lock", "__metadata:\n version: 6\n cacheKey: 8")
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::YarnBerryLike
end
end

it "prioritizes bun.lockb over other lockfiles" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("bun.lockb", "")
File.write("package-lock.json", "{}")
File.write("yarn.lock", "# yarn lockfile v1")
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::BunLike
end
end

it "prioritizes pnpm-lock.yaml over yarn.lock and package-lock.json" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("pnpm-lock.yaml", "lockfileVersion: '6.0'")
File.write("package-lock.json", "{}")
File.write("yarn.lock", "# yarn lockfile v1")
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::PnpmLike
end
end

it "prioritizes yarn.lock over package-lock.json" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("yarn.lock", "# yarn lockfile v1")
File.write("package-lock.json", "{}")
package_json = described_class.read(Dir.pwd, fallback_manager: :bun)

expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
end
end

it "does not add the packageManager property" do
with_package_json_file({ "version" => "1.0.0" }) do
described_class.read(Dir.pwd, fallback_manager: :yarn_classic)
Expand Down Expand Up @@ -306,14 +383,59 @@
end
end

it "uses the fallback manager" do
it "uses the fallback manager when no lockfile is present" do
with_package_json_file({ "version" => "1.0.0" }) do
package_json = described_class.new(fallback_manager: :yarn_classic)

expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
end
end

it "detects npm from package-lock.json" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("package-lock.json", "{}")
package_json = described_class.new(fallback_manager: :yarn_classic)

expect(package_json.manager).to be_a PackageJson::Managers::NpmLike
end
end

it "detects pnpm from pnpm-lock.yaml" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("pnpm-lock.yaml", "lockfileVersion: '6.0'")
package_json = described_class.new(fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::PnpmLike
end
end

it "detects bun from bun.lockb" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("bun.lockb", "")
package_json = described_class.new(fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::BunLike
end
end

it "detects yarn classic from yarn.lock without __metadata:" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("yarn.lock", "# yarn lockfile v1\n\npackage@^1.0.0:\n version \"1.0.0\"")
package_json = described_class.new(fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
end
end

it "detects yarn berry from yarn.lock with __metadata:" do
with_package_json_file({ "version" => "1.0.0" }) do
File.write("yarn.lock", "__metadata:\n version: 6\n cacheKey: 8")
package_json = described_class.new(fallback_manager: :npm)

expect(package_json.manager).to be_a PackageJson::Managers::YarnBerryLike
end
end

it "does not add the packageManager property" do
with_package_json_file({ "version" => "1.0.0" }) do
described_class.new(fallback_manager: :yarn_classic)
Expand Down
Loading