Module: Dependabot::NpmAndYarn::Helpers

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

Overview

rubocop:disable Metrics/ModuleLength

Constant Summary collapse

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

NPM Version Constants

10
NPM_V8 =
8
NPM_V6 =
6
NPM_DEFAULT_VERSION =
NPM_V10
PNPM_V10 =

PNPM Version Constants

10
PNPM_V9 =
9
PNPM_V8 =
8
PNPM_V7 =
7
PNPM_V6 =
6
PNPM_DEFAULT_VERSION =
PNPM_V10
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
SUPPORTED_COREPACK_PACKAGE_MANAGERS =

corepack supported package managers

%w(npm yarn pnpm).freeze

Class Method Summary collapse

Class Method Details

.command_observer(output) ⇒ Object



291
292
293
294
295
296
297
298
299
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 291

def self.command_observer(output)
  # Observe the output for specific error
  return {} unless output.include?("npm ERR! ERESOLVE")

  {
    gracefully_stop: true, # value must be a String
    reason: "NPM Resolution Error"
  }
end

.corepack_supported_package_manager?(name) ⇒ Boolean

Returns:

  • (Boolean)


532
533
534
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 532

def self.corepack_supported_package_manager?(name)
  SUPPORTED_COREPACK_PACKAGE_MANAGERS.include?(name)
end

.dependencies_with_all_versions_metadata(dependency_set) ⇒ Object



524
525
526
527
528
529
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 524

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

.fallback_to_local_version(name) ⇒ Object



405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 405

def self.fallback_to_local_version(name)
  return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)

  Dependabot.logger.info("Falling back to activate the currently installed version of #{name}.")

  # Fetch the currently installed version directly from the environment
  current_version = local_package_manager_version(name)
  Dependabot.logger.info("Activating currently installed version of #{name}: #{current_version}")

  # Prepare the existing version
  package_manager_activate(name, current_version)
end

.fetch_yarnrc_yml_value(key, default_value) ⇒ Object



115
116
117
118
119
120
121
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 115

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



169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 169

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

.install(name, version, env: {}) ⇒ Object



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 374

def self.install(name, version, env: {})
  Dependabot.logger.info("Installing \"#{name}@#{version}\"")

  begin
    # Try to install the specified version
    output = package_manager_install(name, version, env: env)

    # Confirm success based on the output
    if output.match?(/Adding #{name}@.* to the cache/)
      Dependabot.logger.info("#{name}@#{version} successfully installed.")

      Dependabot.logger.info("Activating currently installed version of #{name}: #{version}")
      package_manager_activate(name, version)

    else
      Dependabot.logger.error("Corepack installation output unexpected: #{output}")
      fallback_to_local_version(name)
    end
  rescue StandardError => e
    Dependabot.logger.error("Error installing #{name}@#{version}: #{e.message}")
    fallback_to_local_version(name)
  end

  # Verify the installed version
  installed_version = package_manager_version(name)

  installed_version
end

.local_package_manager_version(name) ⇒ Object



451
452
453
454
455
456
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 451

def self.local_package_manager_version(name)
  Dependabot::SharedHelpers.run_shell_command(
    "#{name} -v",
    fingerprint: "#{name} -v"
  ).strip
end

.node_versionObject



302
303
304
305
306
307
308
309
310
311
312
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 302

def self.node_version
  version = run_node_command("-v", fingerprint: "-v").strip

  # Validate the output format (e.g., "v20.18.1" or "20.18.1")
  if version.match?(/^v?\d+(\.\d+){2}$/)
    version.strip.delete_prefix("v") # Remove the "v" prefix if present
  end
rescue StandardError => e
  Dependabot.logger.error("Error retrieving Node.js version: #{e.message}")
  nil
end

.npm_version_numeric(lockfile) ⇒ Object



44
45
46
47
48
49
50
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 44

def self.npm_version_numeric(lockfile)
  detected_npm_version = detect_npm_version(lockfile)

  return NPM_DEFAULT_VERSION if detected_npm_version.nil? || detected_npm_version == NPM_V6

  detected_npm_version
end

.package_manager_activate(name, version) ⇒ Object



439
440
441
442
443
444
445
446
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 439

def self.package_manager_activate(name, version)
  return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)

  Dependabot::SharedHelpers.run_shell_command(
    "corepack prepare #{name}@#{version} --activate",
    fingerprint: "corepack prepare <name>@<version> --activate"
  ).strip
end

.package_manager_install(name, version, env: {}) ⇒ Object



427
428
429
430
431
432
433
434
435
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 427

def self.package_manager_install(name, version, env: {})
  return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)

  Dependabot::SharedHelpers.run_shell_command(
    "corepack install #{name}@#{version} --global --cache-only",
    fingerprint: "corepack install <name>@<version> --global --cache-only",
    env: env
  ).strip
end

.package_manager_run_command(name, command, fingerprint: nil, output_observer: nil, env: nil) ⇒ Object



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 483

def self.package_manager_run_command(
  name,
  command,
  fingerprint: nil,
  output_observer: nil,
  env: nil
)
  full_command = "corepack #{name} #{command}"
  fingerprint =  "corepack #{name} #{fingerprint || command}"

  if output_observer
    return Dependabot::SharedHelpers.run_shell_command(
      full_command,
      fingerprint: fingerprint,
      output_observer: output_observer,
      env: env
    ).strip
  else
    Dependabot::SharedHelpers.run_shell_command(full_command, fingerprint: fingerprint)
  end.strip
rescue StandardError => e
  Dependabot.logger.error("Error running package manager command: #{full_command}, Error: #{e.message}")
  if e.message.match?(/Response Code.*:.*404.*\(Not Found\)/) &&
     e.message.include?("The remote server failed to provide the requested resource")
    raise RegistryError.new(404, "The remote server failed to provide the requested resource")
  end

  raise
end

.package_manager_version(name) ⇒ Object



460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 460

def self.package_manager_version(name)
  Dependabot.logger.info("Fetching version for package manager: #{name}")

  version = package_manager_run_command(name, "-v").strip

  Dependabot.logger.info("Installed version of #{name}: #{version}")

  version
rescue StandardError => e
  Dependabot.logger.error("Error fetching version for package manager #{name}: #{e.message}")
  raise
end

.parse_npm8?(package_lock) ⇒ Boolean

Returns:

  • (Boolean)


124
125
126
127
128
129
130
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 124

def self.parse_npm8?(package_lock)
  return true unless package_lock&.content

  detected_npm = detect_npm_version(package_lock)
  # For conversion reading properly from npm 6 lockfile we need to check if detected version is npm 6
  detected_npm.nil? || detected_npm != NPM_V6
end

.pnpm_lockfile_version(pnpm_lock) ⇒ Object



516
517
518
519
520
521
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 516

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

  nil
end

.pnpm_version_numeric(pnpm_lock) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 96

def self.pnpm_version_numeric(pnpm_lock)
  lockfile_content = pnpm_lock&.content

  return PNPM_DEFAULT_VERSION if !lockfile_content || lockfile_content.strip.empty?

  pnpm_lockfile_version_str = pnpm_lockfile_version(pnpm_lock)

  return PNPM_FALLBACK_VERSION unless pnpm_lockfile_version_str

  pnpm_lockfile_version = pnpm_lockfile_version_str.to_f

  return PNPM_V10 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_node_command(command, fingerprint: nil) ⇒ Object



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 315

def self.run_node_command(command, fingerprint: nil)
  full_command = "node #{command}"

  Dependabot.logger.info("Running node command: #{full_command}")

  result = Dependabot::SharedHelpers.run_shell_command(
    full_command,
    fingerprint: "node #{fingerprint || command}"
  )

  Dependabot.logger.info("Command executed successfully: #{full_command}")
  result
rescue StandardError => e
  Dependabot.logger.error("Error running node command: #{full_command}, Error: #{e.message}")
  raise
end

.run_npm_command(command, fingerprint: command, env: nil) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 269

def self.run_npm_command(command, fingerprint: command, env: nil)
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
    package_manager_run_command(
      NpmPackageManager::NAME,
      command,
      fingerprint: fingerprint,
      output_observer: ->(output) { command_observer(output) },
      env: env
    )
  else
    Dependabot::SharedHelpers.run_shell_command(
      "npm #{command}",
      fingerprint: "npm #{fingerprint}",
      output_observer: ->(output) { command_observer(output) }
    )
  end
end

.run_pnpm_command(command, fingerprint: nil) ⇒ Object



341
342
343
344
345
346
347
348
349
350
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 341

def self.run_pnpm_command(command, fingerprint: nil)
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
    package_manager_run_command(PNPMPackageManager::NAME, command, fingerprint: fingerprint)
  else
    Dependabot::SharedHelpers.run_shell_command(
      "pnpm #{command}",
      fingerprint: "pnpm #{fingerprint || command}"
    )
  end
end

.run_yarn_command(command, fingerprint: nil) ⇒ Object



334
335
336
337
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 334

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

.run_yarn_commands(*commands) ⇒ Object



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

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

.setup_yarn_berryObject



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 223

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)


218
219
220
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 218

def self.yarn_4_or_higher?
  yarn_major_version >= 4
end

.yarn_berry?(yarn_lock) ⇒ Boolean

Returns:

  • (Boolean)


133
134
135
136
137
138
139
140
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 133

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



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 195

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)


213
214
215
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 213

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

.yarn_berry_skip_build?Boolean

Returns:

  • (Boolean)


208
209
210
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 208

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

.yarn_major_versionObject



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 143

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)


189
190
191
192
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 189

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



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 80

def self.yarn_version_numeric(yarn_lock)
  lockfile_content = yarn_lock&.content

  return YARN_DEFAULT_VERSION if lockfile_content.nil? || lockfile_content.strip.empty?

  if yarn_berry?(yarn_lock)
    YARN_DEFAULT_VERSION
  else
    YARN_FALLBACK_VERSION
  end
end

.yarn_zero_install?Boolean

Returns:

  • (Boolean)


184
185
186
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 184

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