Module: Dependabot::NpmAndYarn::Helpers

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

Overview

rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Classes: ReleaseAgeGateSetting

Constant Summary collapse

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

NPM Version Constants

11
NPM_V10 =
10
NPM_V8 =
8
NPM_V6 =
6
NPM_DEFAULT_VERSION =
NPM_V11
NPM_MINIMUM_RELEASE_AGE_VERSION =

Minimum npm version that supports the --min-release-age CLI flag.

"11.10.0"
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
COREPACK_SIGNATURE_METADATA_ERROR =
"No compatible signature found in package metadata"
MINUTES_PER_DAY =

pnpm's minimumReleaseAge and yarn's npmMinimalAgeGate are expressed in minutes, whereas dependabot.yml cooldown and npm's min-release-age use days. Used to convert a cooldown floor (days) into the minutes those gates expect.

1440

Class Method Summary collapse

Class Method Details

.build_corepack_env_variablesObject



770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 770

def self.build_corepack_env_variables
  return nil unless Dependabot::Experiments.enabled?(:enable_private_registry_for_corepack)
  return nil if dependency_files.nil? || credentials.nil?

  files = T.must(dependency_files)
  creds = T.must(credentials)

  registry_helper = RegistryHelper.new(
    {
      npmrc: files.find { |f| f.name.end_with?(".npmrc") },
      yarnrc: files.find { |f| f.name.end_with?(".yarnrc") && !f.name.end_with?(".yarnrc.yml") },
      yarnrc_yml: files.find { |f| f.name.end_with?(".yarnrc.yml") }
    },
    creds
  )

  registry_helper.find_corepack_env_variables
end

.command_observer(output) ⇒ Object



489
490
491
492
493
494
495
496
497
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 489

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)


865
866
867
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 865

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

.credentialsObject



38
39
40
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 38

def credentials
  T.cast(Thread.current[:npm_and_yarn_credentials], T.nilable(T::Array[Dependabot::Credential]))
end

.credentials=(creds) ⇒ Object



33
34
35
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 33

def credentials=(creds)
  Thread.current[:npm_and_yarn_credentials] = creds
end

.default_npm_registry?(registry) ⇒ Boolean

Returns:

  • (Boolean)


841
842
843
844
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 841

def self.default_npm_registry?(registry)
  normalized_registry = RegistryHelper.normalize_registry_url(registry)
  normalized_registry == RegistryHelper::DEFAULT_NPM_REGISTRY
end

.dependencies_with_all_versions_metadata(dependency_set) ⇒ Object



857
858
859
860
861
862
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 857

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

.dependency_filesObject



28
29
30
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 28

def dependency_files
  T.cast(Thread.current[:npm_and_yarn_dependency_files], T.nilable(T::Array[Dependabot::DependencyFile]))
end

.dependency_files=(files) ⇒ Object



23
24
25
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 23

def dependency_files=(files)
  Thread.current[:npm_and_yarn_dependency_files] = files
end

.fallback_to_local_version(name, env: {}) ⇒ Object



624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 624

def self.fallback_to_local_version(name, env: {})
  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, env: env)
end

.fetch_yarnrc_yml_value(key, default_value) ⇒ Object



289
290
291
292
293
294
295
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 289

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



343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 343

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

.higher_release_age_gate(cooldown, user_gate) ⇒ Object



94
95
96
97
98
99
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 94

def self.higher_release_age_gate(cooldown, user_gate)
  return nil if cooldown.nil? || !cooldown.positive?
  return nil if user_gate && cooldown <= user_gate

  cooldown
end

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



595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 595

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

  begin
    # Try to activate the specified version
    output = package_manager_activate(name, version, env: env)

    # Confirm success based on the output
    if output.include?("immediate activation...")
      Dependabot.logger.info("#{name}@#{version} successfully installed.")

      Dependabot.logger.info("Activating currently installed version of #{name}: #{version}")
    else
      Dependabot.logger.error("Corepack installation output unexpected: #{output}")
      fallback_to_local_version(name, env: env)
    end
  rescue StandardError => e
    Dependabot.logger.error("Error activating #{name}@#{version}: #{e.message}")
    fallback_to_local_version(name, env: env)
  end

  # Verify the installed version
  installed_version = package_manager_version(name, env: env)

  installed_version
end

.local_package_manager_version(name) ⇒ Object



671
672
673
674
675
676
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 671

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

.max_configured_release_age(dependency_files, settings) ⇒ Object



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

def self.max_configured_release_age(dependency_files, settings)
  values = dependency_files.filter_map do |file|
    setting = settings.find { |candidate| candidate.filename == File.basename(file.name) }
    next unless setting

    configured_release_age(file.content.to_s, setting)
  end
  values.max
end

.merge_corepack_env(env) ⇒ Object



761
762
763
764
765
766
767
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 761

def self.merge_corepack_env(env)
  corepack_env = build_corepack_env_variables
  return env if corepack_env.nil? || corepack_env.empty?
  return corepack_env if env.nil?

  corepack_env.merge(env)
end

.node_versionObject



500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 500

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}$/)
    parsed_version = version.strip.delete_prefix("v") # Remove the "v" prefix if present
    Dependabot.logger.info("Using node version: #{parsed_version}")
    parsed_version
  end
rescue StandardError => e
  Dependabot.logger.error("Error retrieving Node.js version: #{e.message}")
  nil
end

.normalize_npm_scope(scope) ⇒ Object



870
871
872
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 870

def self.normalize_npm_scope(scope)
  scope.start_with?("@") ? scope : "@#{scope}"
end

.npm_supports_min_release_age?Boolean

Returns:

  • (Boolean)


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

def self.npm_supports_min_release_age?
  version = npm_version
  return false if version.nil?

  supported = version >= Version.new(NPM_MINIMUM_RELEASE_AGE_VERSION)
  if supported
    Dependabot.logger.info("npm #{version} supports --min-release-age.")
  else
    Dependabot.logger.info(
      "npm #{version} does not support --min-release-age (requires 11.10.0+); the release-age " \
      "cooldown gate will not be applied to transitive dependencies."
    )
  end
  supported
end

.npm_versionObject



256
257
258
259
260
261
262
263
264
265
266
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 256

def self.npm_version
  raw = if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
          package_manager_version(NpmPackageManager::NAME, env: merge_corepack_env(nil))
        else
          local_package_manager_version(NpmPackageManager::NAME)
        end
  Version.new(raw)
rescue StandardError => e
  Dependabot.logger.warn("Could not determine npm version to gate release-age settings: #{e.message}")
  nil
end

.npm_version_numeric(lockfile) ⇒ Object



162
163
164
165
166
167
168
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 162

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, env: {}) ⇒ Object



658
659
660
661
662
663
664
665
666
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 658

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

  run_corepack_command(
    "corepack prepare #{name}@#{version} --activate",
    fingerprint: "corepack prepare <name>@<version> --activate",
    env: env
  )
end

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



646
647
648
649
650
651
652
653
654
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 646

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

  run_corepack_command(
    "corepack install #{name}@#{version} --global --cache-only",
    fingerprint: "corepack install <name>@<version> --global --cache-only",
    env: env
  )
end

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



703
704
705
706
707
708
709
710
711
712
713
714
715
716
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 703

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

.package_manager_version(name, env: nil) ⇒ Object



680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 680

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

  version = package_manager_run_command(name, "-v", env: env).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)


298
299
300
301
302
303
304
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 298

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



849
850
851
852
853
854
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 849

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_versionObject



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 239

def self.pnpm_version
  raw = if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
          package_manager_version(PNPMPackageManager::NAME, env: merge_corepack_env(nil))
        else
          local_package_manager_version(PNPMPackageManager::NAME)
        end
  Version.new(raw)
rescue StandardError => e
  Dependabot.logger.warn("Could not determine pnpm version to gate release-age settings: #{e.message}")
  nil
end

.pnpm_version_numeric(pnpm_lock) ⇒ Object



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

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

.raise_registry_error_if_not_found(error) ⇒ Object



815
816
817
818
819
820
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 815

def self.raise_registry_error_if_not_found(error)
  if error.message.match?(/Response Code.*:.*404.*\(Not Found\)/) &&
     error.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
end

.retry_without_signature_verification?(error:, env:) ⇒ Boolean

Returns:

  • (Boolean)


823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 823

def self.retry_without_signature_verification?(error:, env:)
  return false unless env

  registry = env[RegistryHelper::COREPACK_NPM_REGISTRY_ENV]
  return false unless registry
  return false if default_npm_registry?(registry)
  return false unless error.message.include?(COREPACK_SIGNATURE_METADATA_ERROR)

  # Retry (disabling verification) only when no integrity keys are
  # configured. When a replaces-base registry's merged npm + registry keys
  # were fetched and set, a remaining signature failure is a genuine
  # integrity problem, so we fail closed rather than silently disabling
  # verification. If the keys could not be fetched they are left unset, and
  # this signature-stripping retry still applies as before.
  !env.key?(RegistryHelper::COREPACK_INTEGRITY_KEYS_ENV)
end

.run_corepack_command(full_command, fingerprint:, output_observer: nil, env: nil) ⇒ Object



730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 730

def self.run_corepack_command(full_command, fingerprint:, output_observer: nil, env: nil)
  run_corepack_shell_command(
    full_command,
    fingerprint: fingerprint,
    output_observer: output_observer,
    env: env
  )
rescue StandardError => e
  Dependabot.logger.error("Error running package manager command: #{full_command}, Error: #{e.message}")

  raise_registry_error_if_not_found(e)

  if retry_without_signature_verification?(error: e, env: env)
    retry_env = T.must(env).merge(RegistryHelper::COREPACK_INTEGRITY_KEYS_ENV => "")
    Dependabot.logger.warn(
      "Corepack signature verification failed against the configured private registry. " \
      "Retrying once with COREPACK_INTEGRITY_KEYS disabled for this command only."
    )

    return run_corepack_shell_command(
      full_command,
      fingerprint: fingerprint,
      output_observer: output_observer,
      env: retry_env
    )
  end

  raise
end

.run_corepack_shell_command(full_command, fingerprint:, output_observer:, env: nil) ⇒ Object



797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 797

def self.run_corepack_shell_command(full_command, fingerprint:, output_observer:, env: nil)
  if output_observer
    return Dependabot::SharedHelpers.run_shell_command(
      full_command,
      fingerprint: fingerprint,
      output_observer: output_observer,
      env: env
    ).strip
  end

  Dependabot::SharedHelpers.run_shell_command(
    full_command,
    fingerprint: fingerprint,
    env: env
  ).strip
end

.run_node_command(command, fingerprint: nil) ⇒ Object



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 515

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



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 466

def self.run_npm_command(command, fingerprint: command, env: nil)
  merged_env = merge_corepack_env(env)
  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: merged_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, env: nil) ⇒ Object



553
554
555
556
557
558
559
560
561
562
563
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 553

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

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



540
541
542
543
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 540

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

.run_yarn_commands(*commands, env: nil) ⇒ Object



447
448
449
450
451
452
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 447

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

.setup_yarn_berryObject



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 420

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)


392
393
394
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 392

def self.yarn_4_or_higher?
  yarn_major_version >= 4
end

.yarn_berry?(yarn_lock) ⇒ Boolean

Returns:

  • (Boolean)


307
308
309
310
311
312
313
314
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 307

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



369
370
371
372
373
374
375
376
377
378
379
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 369

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)


387
388
389
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 387

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

.yarn_berry_skip_build?Boolean

Returns:

  • (Boolean)


382
383
384
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 382

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

.yarn_berry_supports_minimal_age_gate?Boolean

Returns:

  • (Boolean)


397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 397

def self.yarn_berry_supports_minimal_age_gate?
  version = Version.new(run_single_yarn_command("--version"))
  supported = version >= Version.new("4.10.0")
  if supported
    Dependabot.logger.info("Yarn #{version} supports npmMinimalAgeGate.")
  else
    Dependabot.logger.info(
      "Yarn #{version} does not support npmMinimalAgeGate (requires 4.10.0+). " \
      "YARN_NPM_MINIMAL_AGE_GATE will not be set."
    )
  end
  supported
rescue StandardError => e
  Dependabot.logger.warn(
    "Could not determine Yarn version to check npmMinimalAgeGate support: #{e.message}. " \
    "Assuming unsupported (returning false). YARN_NPM_MINIMAL_AGE_GATE will not be set, so the " \
    "security-update `=0` bypass cannot be applied — any release-age gate configured in " \
    ".yarnrc.yml (npmMinimalAgeGate) or enforced by the registry may still block security updates."
  )
  false
end

.yarn_major_versionObject



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 317

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)


363
364
365
366
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 363

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



198
199
200
201
202
203
204
205
206
207
208
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 198

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)


358
359
360
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 358

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