@@ -19,40 +19,61 @@ def initialize(dependency_file)
19
19
20
20
sig { returns ( T ::Hash [ String , T . untyped ] ) }
21
21
def parsed
22
+ return @content if @content
23
+
22
24
# Since bun.lock is a JSONC file, which is a subset of YAML, we can use YAML to parse it
23
- json_obj = YAML . load ( T . must ( @dependency_file . content ) )
24
- @parsed ||= T . let ( json_obj , T . nilable ( T ::Hash [ String , T . untyped ] ) )
25
- rescue Psych ::SyntaxError
26
- raise Dependabot ::DependencyFileNotParseable . new ( @dependency_file . path , "Invalid bun.lock file" )
25
+ content = YAML . load ( T . must ( @dependency_file . content ) )
26
+ raise_invalid! ( "expected to be an object" ) unless content . is_a? ( Hash )
27
+
28
+ version = content [ "lockfileVersion" ]
29
+ raise_invalid! ( "expected 'lockfileVersion' to be an integer" ) unless version . is_a? ( Integer )
30
+ raise_invalid! ( "expected 'lockfileVersion' to be >= 0" ) unless version >= 0
31
+ unless version . zero?
32
+ raise_invalid! ( <<~ERROR
33
+ unsupported 'lockfileVersion' = #{ version } , please open an issue with Dependabot to support this:
34
+ https://github.com/dependabot/dependabot/issues/new
35
+ ERROR
36
+ )
37
+ end
38
+
39
+ @content = content
40
+ rescue Psych ::SyntaxError => e
41
+ raise_invalid! ( "malformed JSONC at line #{ e . line } , column #{ e . column } " )
42
+ end
43
+
44
+ sig { returns ( Integer ) }
45
+ def version
46
+ parsed [ "lockfileVersion" ]
27
47
end
28
48
29
49
sig { returns ( Dependabot ::FileParsers ::Base ::DependencySet ) }
30
50
def dependencies
31
51
dependency_set = Dependabot ::FileParsers ::Base ::DependencySet . new
32
52
33
- lockfile_version = parsed [ "lockfileVersion" ]
34
- if lockfile_version . zero?
35
- packages = parsed [ "packages" ]
36
- raise_invalid_lock! ( "expected 'packages' to be an object" ) unless packages . is_a? ( Hash )
53
+ # bun.lock v0 format:
54
+ # https://github.com/oven-sh/bun/blob/c130df6c589fdf28f9f3c7f23ed9901140bc9349/src/install/bun.lock.zig#L595-L605
55
+
56
+ packages = parsed [ "packages" ]
57
+ raise_invalid! ( "expected 'packages' to be an object" ) unless packages . is_a? ( Hash )
37
58
38
- packages . each do |key , details |
39
- raise_invalid_lock !( "expected 'packages.#{ key } ' to be an array" ) unless details . is_a? ( Array )
59
+ packages . each do |key , details |
60
+ raise_invalid !( "expected 'packages.#{ key } ' to be an array" ) unless details . is_a? ( Array )
40
61
41
- entry = details . first
42
- raise_invalid_lock !( "expected 'packages.#{ key } [0]' to be a string" ) unless entry . is_a? ( String )
62
+ resolution = details . first
63
+ raise_invalid !( "expected 'packages.#{ key } [0]' to be a string" ) unless resolution . is_a? ( String )
43
64
44
- name , version = entry . split ( /(?<=\w )\@ / )
45
- next if name . empty? || version . start_with? ( "workspace:" )
65
+ name , version = resolution . split ( /(?<=\w )\@ / )
66
+ next if name . empty?
46
67
47
- dependency_set << Dependency . new (
48
- name : name ,
49
- version : version ,
50
- package_manager : "npm_and_yarn" ,
51
- requirements : [ ]
52
- )
53
- end
54
- else
55
- raise_invalid_lock! ( "expected 'lockfileVersion' to be 0" )
68
+ semver = Version . semver_for ( version )
69
+ next unless semver
70
+
71
+ dependency_set << Dependency . new (
72
+ name : name ,
73
+ version : semver . to_s ,
74
+ package_manager : "npm_and_yarn" ,
75
+ requirements : [ ]
76
+ )
56
77
end
57
78
58
79
dependency_set
@@ -63,9 +84,6 @@ def dependencies
63
84
. returns ( T . nilable ( T ::Hash [ String , T . untyped ] ) )
64
85
end
65
86
def details ( dependency_name , requirement , _manifest_name )
66
- lockfile_version = parsed [ "lockfileVersion" ]
67
- return unless lockfile_version . zero?
68
-
69
87
packages = parsed [ "packages" ]
70
88
return unless packages . is_a? ( Hash )
71
89
@@ -77,39 +95,54 @@ def details(dependency_name, requirement, _manifest_name)
77
95
# If there's only one entry for this dependency, use it, even if
78
96
# the requirement in the lockfile doesn't match
79
97
if candidates . one?
80
- format_details ( lockfile_version , candidates . first )
98
+ parse_details ( candidates . first )
81
99
else
82
100
candidate = candidates . find do |label , _ |
83
101
label . scan ( /(?<=\w )\@ (?:npm:)?([^\s ,]+)/ ) . flatten . include? ( requirement )
84
102
end &.last
85
- format_details ( lockfile_version , candidate )
103
+ parse_details ( candidate )
86
104
end
87
105
end
88
106
89
107
private
90
108
91
109
sig { params ( message : String ) . void }
92
- def raise_invalid_lock !( message )
110
+ def raise_invalid !( message )
93
111
raise Dependabot ::DependencyFileNotParseable . new ( @dependency_file . path , "Invalid bun.lock file: #{ message } " )
94
112
end
95
113
96
114
sig do
97
- params ( lockfile_version : T . nilable ( Integer ) ,
98
- entry : T . nilable ( T ::Array [ T . untyped ] ) ) . returns ( T . nilable ( T ::Hash [ String , T . untyped ] ) )
115
+ params ( entry : T . nilable ( T ::Array [ T . untyped ] ) ) . returns ( T . nilable ( T ::Hash [ String , T . untyped ] ) )
99
116
end
100
- def format_details ( lockfile_version , entry )
101
- return unless lockfile_version . zero?
117
+ def parse_details ( entry )
102
118
return unless entry . is_a? ( Array )
103
119
104
- label , registry , details , hash = entry
105
- name , version = label . split ( /(?<=\w )\@ / )
106
- {
107
- "name" => name ,
108
- "version" => version ,
109
- "registry" => registry ,
110
- "details" => details ,
111
- "hash" => hash
112
- }
120
+ # Either:
121
+ # - "{name}@{version}", registry, details, integrity
122
+ # - "{name}@{resolution}", details
123
+ resolution = entry . first
124
+ return unless resolution . is_a? ( String )
125
+
126
+ name , version = resolution . split ( /(?<=\w )\@ / )
127
+ semver = Version . semver_for ( version )
128
+
129
+ if semver
130
+ registry , details , integrity = entry [ 1 ..3 ]
131
+ {
132
+ "name" => name ,
133
+ "version" => semver . to_s ,
134
+ "registry" => registry ,
135
+ "details" => details ,
136
+ "integrity" => integrity
137
+ }
138
+ else
139
+ details = entry [ 1 ]
140
+ {
141
+ "name" => name ,
142
+ "resolution" => version ,
143
+ "details" => details
144
+ }
145
+ end
113
146
end
114
147
end
115
148
end
0 commit comments