dependabot-deno 0.381.0 → 0.382.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f09b4597272a584e27bbc50f868f2b0f7f7a6fb9c42322a1e05bc4b88de5e81
4
- data.tar.gz: 9a42fd39d620f3f933f17a6fbf4e27ca55eb1a95d83f5e9f8db6174c65e6dc7d
3
+ metadata.gz: 259ff22154909455999bff4c6341ce93bb6cecfdf5ec644535420a0db458c8bf
4
+ data.tar.gz: 7f9fb9b1e67cbbf2daeb3bf0aa6b708cae04976ec26267abdb68d83f0d824511
5
5
  SHA512:
6
- metadata.gz: 406e6856fcf2dc844967f3ccbc027d3800e9e63d5cb9e6d26cc03c8ae6eeefa86ccb4602135b017b483c60119ae94fffa24c78efb29ebe059b35d4aca18b9a47
7
- data.tar.gz: 63ddff74163ba395d05394a791a45f47b7f7d1d00a427f49370493550766eba3b39996133dec8f7984cb727821726083188affc09151de4d6a163e245ab5211d
6
+ metadata.gz: 647e202253c8265d0c294619494a0919aeb4b392e8b0e5f21beef34e8f69f9f76b35e0b1f379f24857f98a42b51931945b8733bb944128b60353b99479b95fe9
7
+ data.tar.gz: c6e93dcfe555ad1ca77a7939289c020166abc0ec959dc34c82c9be969161b6cfd051189026e3988ce9dab43d5d2e27fd4f697bb2f02ff25075b0c2edb8d61c8e
@@ -1,8 +1,9 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/file_fetchers"
5
5
  require "dependabot/file_fetchers/base"
6
+ require "dependabot/deno/helpers"
6
7
 
7
8
  module Dependabot
8
9
  module Deno
@@ -25,8 +26,9 @@ module Dependabot
25
26
  def fetch_files
26
27
  fetched_files = []
27
28
  fetched_files << manifest_file
29
+ fetched_files.concat(workspace_member_files)
28
30
  fetched_files << lockfile if lockfile
29
- fetched_files
31
+ fetched_files.uniq(&:name)
30
32
  end
31
33
 
32
34
  sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -56,6 +58,100 @@ module Dependabot
56
58
  T.nilable(DependencyFile)
57
59
  )
58
60
  end
61
+
62
+ # Fetches the deno.json/deno.jsonc of every workspace member declared in the
63
+ # root manifest's "workspace" field. Members with neither file (e.g.
64
+ # package.json-only members) are skipped.
65
+ sig { returns(T::Array[DependencyFile]) }
66
+ def workspace_member_files
67
+ @workspace_member_files ||= T.let(
68
+ workspace_member_dirs.filter_map { |dir| fetch_member_manifest(dir) },
69
+ T.nilable(T::Array[DependencyFile])
70
+ )
71
+ end
72
+
73
+ sig { params(dir: String).returns(T.nilable(DependencyFile)) }
74
+ def fetch_member_manifest(dir)
75
+ MANIFEST_FILENAMES.filter_map { |f| fetch_file_if_present(File.join(dir, f)) }.first
76
+ end
77
+
78
+ # Resolves the "workspace" field into a concrete list of member directory
79
+ # paths (relative to the repo directory), expanding glob patterns and
80
+ # applying "!" negations. Supports both the array form (["./a", "./b"]) and
81
+ # the legacy object form ({ "members": [...] }).
82
+ sig { returns(T::Array[String]) }
83
+ def workspace_member_dirs
84
+ members = workspace_members
85
+ return [] if members.empty?
86
+
87
+ includes, excludes = members.partition { |m| !m.start_with?("!") }
88
+ excluded = excludes.flat_map { |m| expand_member(m.delete_prefix("!")) }
89
+
90
+ includes
91
+ .flat_map { |m| expand_member(m) }
92
+ .uniq
93
+ .reject { |dir| excluded.include?(dir) }
94
+ # Member paths come from manifest content; never fetch from an absolute
95
+ # path or one that traverses out of the repo via "..".
96
+ .select { |dir| Helpers.safe_relative_path?(dir) }
97
+ end
98
+
99
+ sig { returns(T::Array[String]) }
100
+ def workspace_members
101
+ workspace = Helpers.parse_json_or_jsonc(manifest_file.content).fetch("workspace", nil)
102
+
103
+ case workspace
104
+ when Array then workspace.map(&:to_s)
105
+ when Hash then Array(workspace["members"]).map(&:to_s)
106
+ else []
107
+ end
108
+ end
109
+
110
+ # Expands a single member entry to directory paths. Plain paths are
111
+ # normalised; glob entries (containing "*") are matched against the repo
112
+ # tree, honouring Deno's literal depth semantics (each "/*" = one level).
113
+ sig { params(member: String).returns(T::Array[String]) }
114
+ def expand_member(member)
115
+ normalised = normalise_member_path(member)
116
+ return [normalised] unless normalised.include?("*")
117
+
118
+ expand_glob(normalised)
119
+ end
120
+
121
+ sig { params(member: String).returns(String) }
122
+ def normalise_member_path(member)
123
+ member.delete_prefix("./").delete_suffix("/")
124
+ end
125
+
126
+ # Expands a glob like "packages/*" or "examples/*/*" by walking the repo one
127
+ # directory level per "*" segment. Only directories are kept.
128
+ sig { params(pattern: String).returns(T::Array[String]) }
129
+ def expand_glob(pattern)
130
+ segments = pattern.split("/")
131
+ dirs = T.let([""], T::Array[String])
132
+
133
+ segments.each do |segment|
134
+ dirs = dirs.flat_map do |base|
135
+ if segment == "*"
136
+ child_directories(base)
137
+ else
138
+ child = base.empty? ? segment : File.join(base, segment)
139
+ [child]
140
+ end
141
+ end
142
+ end
143
+
144
+ dirs.reject(&:empty?)
145
+ end
146
+
147
+ sig { params(dir: String).returns(T::Array[String]) }
148
+ def child_directories(dir)
149
+ contents = repo_contents(dir: dir.empty? ? "." : dir, raise_errors: false)
150
+ contents.select { |entry| entry.type == "dir" }
151
+ .map { |entry| dir.empty? ? entry.name : File.join(dir, entry.name) }
152
+ rescue Dependabot::DependencyFileNotFound, Dependabot::DirectoryNotFound
153
+ []
154
+ end
59
155
  end
60
156
  end
61
157
  end
@@ -5,6 +5,7 @@ require "json"
5
5
  require "dependabot/dependency"
6
6
  require "dependabot/file_parsers"
7
7
  require "dependabot/file_parsers/base"
8
+ require "dependabot/deno/helpers"
8
9
  require "dependabot/deno/version"
9
10
 
10
11
  module Dependabot
@@ -20,17 +21,6 @@ module Dependabot
20
21
  JSR_SPECIFIER = %r{\Ajsr:(?<name>@[^@/]+/[^@/]+)(?:@(?<constraint>[^/]+))?(?:/[^\s]*)?\z}
21
22
  NPM_SPECIFIER = %r{\Anpm:(?<name>(?:@[^/]+/)?[^@/]+)(?:@(?<constraint>[^/]+))?(?:/[^\s]*)?\z}
22
23
 
23
- # Matches either a JSON string literal (with escapes), a line comment, a
24
- # block comment, or a trailing comma. The alternation lets gsub preserve
25
- # strings while stripping the JSONC-only constructs, so e.g. "//" inside a
26
- # URL value is not mistaken for the start of a comment.
27
- JSONC_TOKEN = %r{
28
- ("(?:\\.|[^"\\])*") # JSON string literal
29
- | //[^\n]* # line comment
30
- | /\*.*?\*/ # block comment
31
- | ,(?=\s*[\}\]]) # trailing comma
32
- }mx
33
-
34
24
  sig { override.returns(T::Array[Dependabot::Dependency]) }
35
25
  def parse
36
26
  # Multiple import aliases can reference the same underlying package
@@ -43,22 +33,24 @@ module Dependabot
43
33
  # can update them all.
44
34
  deps_by_key = {}
45
35
 
46
- imports.each do |_alias_name, specifier|
47
- dep = parse_specifier(specifier.to_s)
48
- next unless dep
49
-
50
- key = [dep.name, T.must(dep.requirements.first)[:source][:type]]
51
- existing = deps_by_key[key]
52
- deps_by_key[key] = if existing
53
- Dependabot::Dependency.new(
54
- name: existing.name,
55
- version: existing.version,
56
- requirements: (existing.requirements + dep.requirements).uniq,
57
- package_manager: existing.package_manager
58
- )
59
- else
60
- dep
61
- end
36
+ manifest_files.each do |file|
37
+ imports_for(file).each do |_alias_name, specifier|
38
+ dep = parse_specifier(specifier.to_s, file)
39
+ next unless dep
40
+
41
+ key = [dep.name, T.must(dep.requirements.first)[:source][:type]]
42
+ existing = deps_by_key[key]
43
+ deps_by_key[key] = if existing
44
+ Dependabot::Dependency.new(
45
+ name: existing.name,
46
+ version: existing.version,
47
+ requirements: (existing.requirements + dep.requirements).uniq,
48
+ package_manager: existing.package_manager
49
+ )
50
+ else
51
+ dep
52
+ end
53
+ end
62
54
  end
63
55
 
64
56
  deps_by_key.values.sort_by(&:name)
@@ -68,51 +60,55 @@ module Dependabot
68
60
 
69
61
  sig { override.void }
70
62
  def check_required_files
71
- return if manifest_file
63
+ return if manifest_files.any?
72
64
 
73
65
  raise "No deno.json or deno.jsonc found!"
74
66
  end
75
67
 
76
- sig { returns(T::Hash[String, T.untyped]) }
77
- def imports
78
- parsed_manifest.fetch("imports", {})
79
- end
80
-
81
- sig { returns(T::Hash[String, T.untyped]) }
82
- def parsed_manifest
83
- @parsed_manifest ||= T.let(
84
- parse_json_or_jsonc(T.must(manifest_file).content),
85
- T.nilable(T::Hash[String, T.untyped])
68
+ # The root manifest plus every workspace member manifest. Members are
69
+ # fetched relative to the root (e.g. "packages/foo/deno.json"), so match
70
+ # on basename rather than the full path.
71
+ sig { returns(T::Array[DependencyFile]) }
72
+ def manifest_files
73
+ @manifest_files ||= T.let(
74
+ dependency_files.select { |f| MANIFEST_FILENAMES.include?(File.basename(f.name)) },
75
+ T.nilable(T::Array[DependencyFile])
86
76
  )
87
77
  end
88
78
 
89
- sig { returns(T.nilable(DependencyFile)) }
90
- def manifest_file
91
- @manifest_file ||= T.let(
92
- MANIFEST_FILENAMES.filter_map { |f| get_original_file(f) }.first,
93
- T.nilable(DependencyFile)
94
- )
79
+ sig { params(file: DependencyFile).returns(T::Hash[String, T.untyped]) }
80
+ def imports_for(file)
81
+ Helpers.parse_json_or_jsonc(file.content).fetch("imports", {})
95
82
  end
96
83
 
97
- sig { params(specifier: String).returns(T.nilable(Dependabot::Dependency)) }
98
- def parse_specifier(specifier)
84
+ sig { params(specifier: String, file: DependencyFile).returns(T.nilable(Dependabot::Dependency)) }
85
+ def parse_specifier(specifier, file)
99
86
  if (match = JSR_SPECIFIER.match(specifier))
100
87
  build_dependency(
101
88
  name: T.must(match[:name]),
102
89
  constraint: match[:constraint],
103
- source_type: "jsr"
90
+ source_type: "jsr",
91
+ file: file
104
92
  )
105
93
  elsif (match = NPM_SPECIFIER.match(specifier))
106
94
  build_dependency(
107
95
  name: T.must(match[:name]),
108
96
  constraint: match[:constraint],
109
- source_type: "npm"
97
+ source_type: "npm",
98
+ file: file
110
99
  )
111
100
  end
112
101
  end
113
102
 
114
- sig { params(name: String, constraint: T.nilable(String), source_type: String).returns(Dependabot::Dependency) }
115
- def build_dependency(name:, constraint:, source_type:)
103
+ sig do
104
+ params(
105
+ name: String,
106
+ constraint: T.nilable(String),
107
+ source_type: String,
108
+ file: DependencyFile
109
+ ).returns(Dependabot::Dependency)
110
+ end
111
+ def build_dependency(name:, constraint:, source_type:, file:)
116
112
  version = constraint ? extract_version(constraint) : nil
117
113
 
118
114
  Dependabot::Dependency.new(
@@ -120,7 +116,7 @@ module Dependabot
120
116
  version: version,
121
117
  requirements: [{
122
118
  requirement: constraint,
123
- file: T.must(manifest_file).name,
119
+ file: file.name,
124
120
  groups: ["imports"],
125
121
  source: { type: source_type }
126
122
  }],
@@ -135,15 +131,6 @@ module Dependabot
135
131
 
136
132
  nil
137
133
  end
138
-
139
- sig { params(content: T.nilable(String)).returns(T::Hash[String, T.untyped]) }
140
- def parse_json_or_jsonc(content)
141
- return {} unless content
142
-
143
- cleaned = content.gsub(JSONC_TOKEN) { ::Regexp.last_match(1) || "" }
144
-
145
- JSON.parse(cleaned)
146
- end
147
134
  end
148
135
  end
149
136
  end
@@ -1,6 +1,7 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "fileutils"
4
5
  require "json"
5
6
  require "sorbet-runtime"
6
7
 
@@ -89,20 +90,35 @@ module Dependabot
89
90
 
90
91
  sig { params(dir: String).void }
91
92
  def write_temporary_files(dir)
92
- File.write(File.join(dir, manifest.name), updated_manifest_content)
93
+ manifest_files.each do |file|
94
+ # Defence in depth: manifest names ultimately derive from workspace
95
+ # member paths in repo content. Refuse to write through an absolute
96
+ # name or one containing ".." so lockfile regeneration can't become a
97
+ # write primitive outside the temporary directory.
98
+ unless Helpers.safe_relative_path?(file.name)
99
+ raise Dependabot::DependencyFileNotResolvable,
100
+ "Unsafe manifest path: #{file.name}"
101
+ end
102
+
103
+ path = File.join(dir, file.name)
104
+ FileUtils.mkdir_p(File.dirname(path))
105
+ File.write(path, updated_manifest_content(file))
106
+ end
93
107
  File.write(File.join(dir, LOCKFILE_FILENAME), T.must(lockfile.content))
94
108
  end
95
109
 
96
- sig { returns(String) }
97
- def updated_manifest_content
98
- ManifestUpdater.new(dependencies: dependencies, manifest: manifest).updated_manifest_content
110
+ sig { params(file: Dependabot::DependencyFile).returns(String) }
111
+ def updated_manifest_content(file)
112
+ ManifestUpdater.new(dependencies: dependencies, manifest: file).updated_manifest_content
99
113
  end
100
114
 
101
- sig { returns(Dependabot::DependencyFile) }
102
- def manifest
103
- @manifest ||= T.let(
104
- T.must(dependency_files.find { |f| FileUpdater::MANIFEST_FILENAMES.include?(f.name) }),
105
- T.nilable(Dependabot::DependencyFile)
115
+ # The root manifest plus every workspace member manifest. Members are
116
+ # fetched relative to the root, so match on basename rather than path.
117
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
118
+ def manifest_files
119
+ @manifest_files ||= T.let(
120
+ dependency_files.select { |f| FileUpdater::MANIFEST_FILENAMES.include?(File.basename(f.name)) },
121
+ T.nilable(T::Array[Dependabot::DependencyFile])
106
122
  )
107
123
  end
108
124
 
@@ -19,7 +19,7 @@ module Dependabot
19
19
  updated_files = []
20
20
 
21
21
  dependency_files.each do |file|
22
- next unless MANIFEST_FILENAMES.include?(file.name)
22
+ next unless MANIFEST_FILENAMES.include?(File.basename(file.name))
23
23
 
24
24
  new_content = update_manifest_content(file)
25
25
  next if new_content == file.content
@@ -41,7 +41,7 @@ module Dependabot
41
41
 
42
42
  sig { override.void }
43
43
  def check_required_files
44
- return if dependency_files.any? { |f| MANIFEST_FILENAMES.include?(f.name) }
44
+ return if dependency_files.any? { |f| MANIFEST_FILENAMES.include?(File.basename(f.name)) }
45
45
 
46
46
  raise "No deno.json or deno.jsonc found!"
47
47
  end
@@ -1,6 +1,8 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "json"
5
+ require "pathname"
4
6
  require "sorbet-runtime"
5
7
 
6
8
  require "dependabot/shared_helpers"
@@ -10,6 +12,47 @@ module Dependabot
10
12
  module Helpers
11
13
  extend T::Sig
12
14
 
15
+ # Matches either a JSON string literal (with escapes), a line comment, a
16
+ # block comment, or a trailing comma. The alternation lets gsub preserve
17
+ # strings while stripping the JSONC-only constructs, so e.g. "//" inside a
18
+ # URL value is not mistaken for the start of a comment.
19
+ JSONC_TOKEN = T.let(
20
+ %r{
21
+ ("(?:\\.|[^"\\])*") # JSON string literal
22
+ | //[^\n]* # line comment
23
+ | /\*.*?\*/ # block comment
24
+ | ,(?=\s*[\}\]]) # trailing comma
25
+ }mx,
26
+ Regexp
27
+ )
28
+
29
+ sig { params(content: T.nilable(String)).returns(T::Hash[String, T.untyped]) }
30
+ def self.parse_json_or_jsonc(content)
31
+ return {} unless content
32
+
33
+ cleaned = content.gsub(JSONC_TOKEN) { ::Regexp.last_match(1) || "" }
34
+
35
+ parsed = JSON.parse(cleaned)
36
+ # A deno.json(c) must be a JSON object. Guard here so a malformed manifest
37
+ # (e.g. a top-level array) surfaces as a clear parse error rather than an
38
+ # opaque sorbet-runtime type error at the call site.
39
+ raise JSON::ParserError, "Expected a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
40
+
41
+ parsed
42
+ end
43
+
44
+ # True when `path` is a repo-relative path with no traversal. Workspace
45
+ # member paths are derived from manifest content, so absolute paths
46
+ # ("/etc") or ".." segments must never be used as fetch/write targets —
47
+ # File.join would otherwise escape the repo checkout or temp directory.
48
+ sig { params(path: String).returns(T::Boolean) }
49
+ def self.safe_relative_path?(path)
50
+ return false if path.empty?
51
+ return false if Pathname.new(path).absolute?
52
+
53
+ Pathname.new(path).each_filename.none?("..")
54
+ end
55
+
13
56
  # Wraps `deno <args>` via Dependabot's standard subprocess helper, so
14
57
  # failures surface as Dependabot::SharedHelpers::HelperSubprocessFailed
15
58
  # (consistent with cargo / bun / npm_and_yarn). DENO_DIR is scoped to
@@ -28,13 +28,14 @@ module Dependabot
28
28
  dependency.version
29
29
  end
30
30
 
31
- sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
31
+ sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
32
32
  def updated_requirements
33
33
  return dependency.requirements unless latest_version
34
34
 
35
- dependency.requirements.map do |req|
35
+ updated = dependency.requirements.map do |req|
36
36
  req.merge(requirement: updated_constraint(req[:requirement]))
37
37
  end
38
+ wrap_requirements(updated)
38
39
  end
39
40
 
40
41
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-deno
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.381.0
4
+ version: 0.382.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.381.0
18
+ version: 0.382.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.381.0
25
+ version: 0.382.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -259,7 +259,7 @@ licenses:
259
259
  - MIT
260
260
  metadata:
261
261
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
262
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.381.0
262
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.382.0
263
263
  rdoc_options: []
264
264
  require_paths:
265
265
  - lib