Module: Dependabot::NpmAndYarn::Helpers

Extended by:
T::Sig
Defined in:
lib/dependabot/npm_and_yarn/helpers.rb

Constant Summary collapse

YARN_PATH_NOT_FOUND =
/^.*(?<error>The "yarn-path" option has been set \(in [^)]+\), but the specified location doesn't exist)/
NPM_V8 =

NPM Version Constants

8
NPM_V6 =
6
NPM_DEFAULT_VERSION =
NPM_V8
PNPM_V9 =

PNPM Version Constants

9
PNPM_V8 =
8
PNPM_V7 =
7
PNPM_V6 =
6
PNPM_DEFAULT_VERSION =
PNPM_V9
PNPM_FALLBACK_VERSION =
PNPM_V6
YARN_V3 =

YARN Version Constants

3
YARN_V2 =
2
YARN_V1 =
1
YARN_DEFAULT_VERSION =
YARN_V3
YARN_FALLBACK_VERSION =
YARN_V1

Class Method Summary collapse

Class Method Details

.dependencies_with_all_versions_metadata(dependency_set) ⇒ Object



276
277
278
279
280
281
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 276

def self.(dependency_set)
  dependency_set.dependencies.map do |dependency|
    dependency.[:all_versions] = dependency_set.all_versions_for_name(dependency.name)
    dependency
  end
end

.fetch_yarnrc_yml_value(key, default_value) ⇒ Object



110
111
112
113
114
115
116
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 110

def self.fetch_yarnrc_yml_value(key, default_value)
  if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml"))
    yarnrc.fetch(key, default_value)
  else
    default_value
  end
end

.handle_subprocess_failure(error) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 161

def self.handle_subprocess_failure(error)
  message = error.message
  if YARN_PATH_NOT_FOUND.match?(message)
    error = T.must(T.must(YARN_PATH_NOT_FOUND.match(message))[:error]).sub(Dir.pwd, ".")
    raise MisconfiguredTooling.new("Yarn", error)
  end

  if message.include?("Internal Error") && message.include?(".yarnrc.yml")
    raise MisconfiguredTooling.new("Invalid .yarnrc.yml file", message)
  end

  raise
end

.npm8?(package_lock) ⇒ Boolean

Returns:

  • (Boolean)


119
120
121
122
123
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 119

def self.npm8?(package_lock)
  return true unless package_lock

  npm_version_numeric(package_lock) == NPM_V8
end

.npm_version_numeric(lockfile) ⇒ Object



41
42
43
44
45
46
47
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 41

def self.npm_version_numeric(lockfile)
  fallback_version_npm8 = Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6)

  return npm_version_numeric_npm8_or_higher(lockfile) if fallback_version_npm8

  npm_version_numeric_npm6_or_higher(lockfile)
end

.npm_version_numeric_npm6_or_higher(lockfile) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 50

def self.npm_version_numeric_npm6_or_higher(lockfile)
  lockfile_content = T.must(lockfile.content)
  return NPM_V8 if JSON.parse(lockfile_content)["lockfileVersion"].to_i >= 2

  NPM_V6
rescue JSON::ParserError
  NPM_V6
end

.npm_version_numeric_npm8_or_higher(lockfile) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 64

def self.npm_version_numeric_npm8_or_higher(lockfile)
  lockfile_content = lockfile.content

  # Return default NPM version if there's no lockfile or it's empty
  return NPM_DEFAULT_VERSION if lockfile_content.nil? || lockfile_content.strip.empty?

  parsed_lockfile = JSON.parse(lockfile_content)

  lockfile_version_str = parsed_lockfile["lockfileVersion"]

  # Default to npm default version if lockfileVersion is missing or empty
  return NPM_DEFAULT_VERSION if lockfile_version_str.nil? || lockfile_version_str.to_s.strip.empty?

  lockfile_version = lockfile_version_str.to_i

  # Using npm 8 as the default for lockfile_version > 2.
  # Update needed to support npm 9+ based on lockfile version.
  return NPM_V8 if lockfile_version >= 2

  NPM_DEFAULT_VERSION
rescue JSON::ParserError
  NPM_DEFAULT_VERSION # Fallback to default npm version if parsing fails
end

.pnpm_lockfile_version(pnpm_lock) ⇒ Object



271
272
273
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 271

def self.pnpm_lockfile_version(pnpm_lock)
  pnpm_lock.content.match(/^lockfileVersion: ['"]?(?<version>[\d.]+)/)[:version]
end

.pnpm_version_numeric(pnpm_lock) ⇒ Object



101
102
103
104
105
106
107
108
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 101

def self.pnpm_version_numeric(pnpm_lock)
  pnpm_lockfile_version = pnpm_lockfile_version(pnpm_lock).to_f
  return PNPM_V9 if pnpm_lockfile_version >= 9.0
  return PNPM_V8 if pnpm_lockfile_version >= 6.0
  return PNPM_V7 if pnpm_lockfile_version >= 5.4

  PNPM_FALLBACK_VERSION
end

.run_npm_command(command, fingerprint: command) ⇒ Object

Run single npm command returning stdout/stderr.

NOTE: Needs to be explicitly run through corepack to respect the ‘packageManager` setting in `package.json`, because corepack does not add shims for NPM.



250
251
252
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 250

def self.run_npm_command(command, fingerprint: command)
  SharedHelpers.run_shell_command("corepack npm #{command}", fingerprint: "corepack npm #{fingerprint}")
end

.run_pnpm_command(command, fingerprint: nil) ⇒ Object

Run single pnpm command returning stdout/stderr



261
262
263
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 261

def self.run_pnpm_command(command, fingerprint: nil)
  SharedHelpers.run_shell_command("pnpm #{command}", fingerprint: "pnpm #{fingerprint || command}")
end

.run_yarn_command(command, fingerprint: nil) ⇒ Object

Setup yarn and run a single yarn command returning stdout/stderr



255
256
257
258
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 255

def self.run_yarn_command(command, fingerprint: nil)
  setup_yarn_berry
  run_single_yarn_command(command, fingerprint: fingerprint)
end

.run_yarn_commands(*commands) ⇒ Object

Run any number of yarn commands while ensuring that ‘enableScripts` is set to false. Yarn commands should not be ran outside of this helper to ensure that postinstall scripts are never executed, as they could contain malicious code.



240
241
242
243
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 240

def self.run_yarn_commands(*commands)
  setup_yarn_berry
  commands.each { |cmd, fingerprint| run_single_yarn_command(cmd, fingerprint: fingerprint) }
end

.setup_yarn_berryObject



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 214

def self.setup_yarn_berry
  # Always disable immutable installs so yarn's CI detection doesn't prevent updates.
  run_single_yarn_command("config set enableImmutableInstalls false")
  # Do not generate a cache if offline cache disabled. Otherwise side effects may confuse further checks
  run_single_yarn_command("config set enableGlobalCache true") unless yarn_berry_skip_build?
  # We never want to execute postinstall scripts, either set this config or mode=skip-build must be set
  run_single_yarn_command("config set enableScripts false") if yarn_berry_disable_scripts?
  if (http_proxy = ENV.fetch("HTTP_PROXY", false))
    run_single_yarn_command("config set httpProxy #{http_proxy}", fingerprint: "config set httpProxy <proxy>")
  end
  if (https_proxy = ENV.fetch("HTTPS_PROXY", false))
    run_single_yarn_command("config set httpsProxy #{https_proxy}", fingerprint: "config set httpsProxy <proxy>")
  end
  return unless (ca_file_path = ENV.fetch("NODE_EXTRA_CA_CERTS", false))

  if yarn_4_or_higher?
    run_single_yarn_command("config set httpsCaFilePath #{ca_file_path}")
  else
    run_single_yarn_command("config set caFilePath #{ca_file_path}")
  end
end

.yarn_4_or_higher?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 210

def self.yarn_4_or_higher?
  yarn_major_version >= 4
end

.yarn_berry?(yarn_lock) ⇒ Boolean

Returns:

  • (Boolean)


126
127
128
129
130
131
132
133
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 126

def self.yarn_berry?(yarn_lock)
  return false if yarn_lock.nil? || yarn_lock.content.nil?

  yaml = YAML.safe_load(T.must(yarn_lock.content))
  yaml.key?("__metadata")
rescue StandardError
  false
end

.yarn_berry_argsObject



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 187

def self.yarn_berry_args
  if yarn_major_version == 2
    ""
  elsif yarn_berry_skip_build?
    "--mode=skip-build"
  else
    # We only want this mode if the cache is not being updated/managed
    # as this improperly leaves old versions in the cache
    "--mode=update-lockfile"
  end
end

.yarn_berry_disable_scripts?Boolean

Returns:

  • (Boolean)


205
206
207
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 205

def self.yarn_berry_disable_scripts?
  yarn_major_version == YARN_V2 || !yarn_zero_install?
end

.yarn_berry_skip_build?Boolean

Returns:

  • (Boolean)


200
201
202
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 200

def self.yarn_berry_skip_build?
  yarn_major_version >= YARN_V3 && (yarn_zero_install? || yarn_offline_cache?)
end

.yarn_major_versionObject



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 136

def self.yarn_major_version
  retries = 0
  output = run_single_yarn_command("--version")
  Version.new(output).major
rescue Dependabot::SharedHelpers::HelperSubprocessFailed => e
  # Should never happen, can probably be removed once this settles
  raise "Failed to replace ENV, not sure why" if T.must(retries).positive?

  message = e.message

  missing_env_var_regex = %r{Environment variable not found \((?:[^)]+)\) in #{Dir.pwd}/(?<path>\S+)}

  if message.match?(missing_env_var_regex)
    match = T.must(message.match(missing_env_var_regex))
    path = T.must(match.named_captures["path"])

    File.write(path, File.read(path).gsub(/\$\{[^}-]+\}/, ""))
    retries = T.must(retries) + 1

    retry
  end

  handle_subprocess_failure(e)
end

.yarn_offline_cache?Boolean

Returns:

  • (Boolean)


181
182
183
184
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 181

def self.yarn_offline_cache?
  yarn_cache_dir = fetch_yarnrc_yml_value("cacheFolder", ".yarn/cache")
  File.exist?(yarn_cache_dir) && (fetch_yarnrc_yml_value("nodeLinker", "") == "node-modules")
end

.yarn_version_numeric(yarn_lock) ⇒ Object



89
90
91
92
93
94
95
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 89

def self.yarn_version_numeric(yarn_lock)
  if yarn_berry?(yarn_lock)
    YARN_DEFAULT_VERSION
  else
    YARN_FALLBACK_VERSION
  end
end

.yarn_zero_install?Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 176

def self.yarn_zero_install?
  File.exist?(".pnp.cjs")
end