Skip to content

Commit e9da837

Browse files
justin808claude
andcommitted
feat: detect package manager from lockfiles
Add automatic package manager detection based on lockfile presence when the packageManager property is not set in package.json. This provides a better default experience by using the package manager that's already in use in the project. Detection priority: 1. bun.lockb - Bun 2. pnpm-lock.yaml - pnpm 3. yarn.lock - Yarn (detects Berry vs Classic from file format) 4. package-lock.json - npm If no lockfile is found, falls back to PACKAGE_JSON_FALLBACK_MANAGER environment variable or npm. Fixes #22 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 4337430 commit e9da837

3 files changed

Lines changed: 183 additions & 7 deletions

File tree

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,18 @@ in the `package.json`.
8585
> `package.json`, and it is up to the developer to ensure that results in the
8686
> desired package manager actually running.
8787
88-
If the `packageManager` property is not present, then the fallback manager will
89-
be used; this defaults to the value of the `PACKAGE_JSON_FALLBACK_MANAGER`
90-
environment variable or otherwise `npm`. You can also provide a specific
91-
fallback manager:
88+
If the `packageManager` property is not present, the gem will automatically
89+
detect which package manager to use by checking for lockfiles in this priority
90+
order:
91+
92+
1. `bun.lockb` - Bun
93+
2. `pnpm-lock.yaml` - pnpm
94+
3. `yarn.lock` - Yarn (Berry or Classic, determined by file format)
95+
4. `package-lock.json` - npm
96+
97+
If no lockfile is found, then the fallback manager will be used; this defaults
98+
to the value of the `PACKAGE_JSON_FALLBACK_MANAGER` environment variable or
99+
otherwise `npm`. You can also provide a specific fallback manager:
92100

93101
```ruby
94102
PackageJson.read(fallback_manager: :pnpm)

lib/package_json.rb

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,61 @@ def record_package_manager!
7575
def determine_package_manager(fallback_manager)
7676
package_manager = fetch("packageManager", nil)
7777

78-
return fallback_manager if package_manager.nil?
78+
return parse_package_manager(package_manager) unless package_manager.nil?
7979

80+
# If no packageManager property, check for lockfiles
81+
lockfile_manager = detect_manager_from_lockfile
82+
83+
return lockfile_manager unless lockfile_manager.nil?
84+
85+
# Fall back to the provided fallback manager
86+
fallback_manager
87+
end
88+
89+
def parse_package_manager(package_manager)
8090
name, version = package_manager.split("@")
8191

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

8494
name.to_sym
8595
end
8696

97+
def detect_manager_from_lockfile
98+
# Check for lockfiles in priority order
99+
# bun.lockb - Bun
100+
return :bun if File.exist?("#{directory}/bun.lockb")
101+
102+
# pnpm-lock.yaml - pnpm
103+
return :pnpm if File.exist?("#{directory}/pnpm-lock.yaml")
104+
105+
# yarn.lock - Yarn (need to distinguish between Berry and Classic)
106+
if File.exist?("#{directory}/yarn.lock")
107+
return detect_yarn_version_from_lockfile
108+
end
109+
110+
# package-lock.json - npm
111+
return :npm if File.exist?("#{directory}/package-lock.json")
112+
113+
# No lockfile found
114+
nil
115+
end
116+
117+
def detect_yarn_version_from_lockfile
118+
lockfile_path = "#{directory}/yarn.lock"
119+
return nil unless File.exist?(lockfile_path)
120+
121+
# Read the first few lines to determine the version
122+
# Yarn Berry lockfiles start with "__metadata:" or have "# yarn lockfile v1" but use a different format
123+
# Yarn Classic lockfiles start with "# THIS IS AN AUTOGENERATED FILE" and "# yarn lockfile v1"
124+
content = File.read(lockfile_path, 1000) # Read first 1000 chars
125+
126+
# Yarn Berry uses __metadata: at the start
127+
return :yarn_berry if content.include?("__metadata:")
128+
129+
# Default to Yarn Classic for older format
130+
:yarn_classic
131+
end
132+
87133
def determine_yarn_version(version)
88134
raise Error, "a major version must be present for Yarn" if version.nil? || version.empty?
89135

spec/package_json_spec.rb

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,91 @@
136136
end
137137
end
138138

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

143143
expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
144144
end
145145
end
146146

147+
it "detects npm from package-lock.json" do
148+
with_package_json_file({ "version" => "1.0.0" }) do
149+
File.write("package-lock.json", "{}")
150+
package_json = described_class.read(Dir.pwd, fallback_manager: :yarn_classic)
151+
152+
expect(package_json.manager).to be_a PackageJson::Managers::NpmLike
153+
end
154+
end
155+
156+
it "detects pnpm from pnpm-lock.yaml" do
157+
with_package_json_file({ "version" => "1.0.0" }) do
158+
File.write("pnpm-lock.yaml", "lockfileVersion: '6.0'")
159+
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)
160+
161+
expect(package_json.manager).to be_a PackageJson::Managers::PnpmLike
162+
end
163+
end
164+
165+
it "detects bun from bun.lockb" do
166+
with_package_json_file({ "version" => "1.0.0" }) do
167+
File.write("bun.lockb", "")
168+
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)
169+
170+
expect(package_json.manager).to be_a PackageJson::Managers::BunLike
171+
end
172+
end
173+
174+
it "detects yarn classic from yarn.lock without __metadata:" do
175+
with_package_json_file({ "version" => "1.0.0" }) do
176+
File.write("yarn.lock", "# yarn lockfile v1\n\npackage@^1.0.0:\n version \"1.0.0\"")
177+
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)
178+
179+
expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
180+
end
181+
end
182+
183+
it "detects yarn berry from yarn.lock with __metadata:" do
184+
with_package_json_file({ "version" => "1.0.0" }) do
185+
File.write("yarn.lock", "__metadata:\n version: 6\n cacheKey: 8")
186+
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)
187+
188+
expect(package_json.manager).to be_a PackageJson::Managers::YarnBerryLike
189+
end
190+
end
191+
192+
it "prioritizes bun.lockb over other lockfiles" do
193+
with_package_json_file({ "version" => "1.0.0" }) do
194+
File.write("bun.lockb", "")
195+
File.write("package-lock.json", "{}")
196+
File.write("yarn.lock", "# yarn lockfile v1")
197+
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)
198+
199+
expect(package_json.manager).to be_a PackageJson::Managers::BunLike
200+
end
201+
end
202+
203+
it "prioritizes pnpm-lock.yaml over yarn.lock and package-lock.json" do
204+
with_package_json_file({ "version" => "1.0.0" }) do
205+
File.write("pnpm-lock.yaml", "lockfileVersion: '6.0'")
206+
File.write("package-lock.json", "{}")
207+
File.write("yarn.lock", "# yarn lockfile v1")
208+
package_json = described_class.read(Dir.pwd, fallback_manager: :npm)
209+
210+
expect(package_json.manager).to be_a PackageJson::Managers::PnpmLike
211+
end
212+
end
213+
214+
it "prioritizes yarn.lock over package-lock.json" do
215+
with_package_json_file({ "version" => "1.0.0" }) do
216+
File.write("yarn.lock", "# yarn lockfile v1")
217+
File.write("package-lock.json", "{}")
218+
package_json = described_class.read(Dir.pwd, fallback_manager: :bun)
219+
220+
expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
221+
end
222+
end
223+
147224
it "does not add the packageManager property" do
148225
with_package_json_file({ "version" => "1.0.0" }) do
149226
described_class.read(Dir.pwd, fallback_manager: :yarn_classic)
@@ -306,14 +383,59 @@
306383
end
307384
end
308385

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

313390
expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
314391
end
315392
end
316393

394+
it "detects npm from package-lock.json" do
395+
with_package_json_file({ "version" => "1.0.0" }) do
396+
File.write("package-lock.json", "{}")
397+
package_json = described_class.new(fallback_manager: :yarn_classic)
398+
399+
expect(package_json.manager).to be_a PackageJson::Managers::NpmLike
400+
end
401+
end
402+
403+
it "detects pnpm from pnpm-lock.yaml" do
404+
with_package_json_file({ "version" => "1.0.0" }) do
405+
File.write("pnpm-lock.yaml", "lockfileVersion: '6.0'")
406+
package_json = described_class.new(fallback_manager: :npm)
407+
408+
expect(package_json.manager).to be_a PackageJson::Managers::PnpmLike
409+
end
410+
end
411+
412+
it "detects bun from bun.lockb" do
413+
with_package_json_file({ "version" => "1.0.0" }) do
414+
File.write("bun.lockb", "")
415+
package_json = described_class.new(fallback_manager: :npm)
416+
417+
expect(package_json.manager).to be_a PackageJson::Managers::BunLike
418+
end
419+
end
420+
421+
it "detects yarn classic from yarn.lock without __metadata:" do
422+
with_package_json_file({ "version" => "1.0.0" }) do
423+
File.write("yarn.lock", "# yarn lockfile v1\n\npackage@^1.0.0:\n version \"1.0.0\"")
424+
package_json = described_class.new(fallback_manager: :npm)
425+
426+
expect(package_json.manager).to be_a PackageJson::Managers::YarnClassicLike
427+
end
428+
end
429+
430+
it "detects yarn berry from yarn.lock with __metadata:" do
431+
with_package_json_file({ "version" => "1.0.0" }) do
432+
File.write("yarn.lock", "__metadata:\n version: 6\n cacheKey: 8")
433+
package_json = described_class.new(fallback_manager: :npm)
434+
435+
expect(package_json.manager).to be_a PackageJson::Managers::YarnBerryLike
436+
end
437+
end
438+
317439
it "does not add the packageManager property" do
318440
with_package_json_file({ "version" => "1.0.0" }) do
319441
described_class.new(fallback_manager: :yarn_classic)

0 commit comments

Comments
 (0)