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_V11 =

NPM Version Constants

11
NPM_V10 =
10
NPM_V8 =
8
NPM_V6 =
6
NPM_DEFAULT_VERSION =
NPM_V11
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

.build_corepack_env_variablesObject



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 556

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



321
322
323
324
325
326
327
328
329
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 321

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)


594
595
596
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 594

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

.dependencies_with_all_versions_metadata(dependency_set) ⇒ Object



586
587
588
589
590
591
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 586

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



433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 433

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



144
145
146
147
148
149
150
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 144

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



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

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



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 404

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



480
481
482
483
484
485
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 480

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

.merge_corepack_env(env) ⇒ Object



547
548
549
550
551
552
553
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 547

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



332
333
334
335
336
337
338
339
340
341
342
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 332

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



73
74
75
76
77
78
79
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 73

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



467
468
469
470
471
472
473
474
475
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 467

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

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



455
456
457
458
459
460
461
462
463
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 455

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



512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 512

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



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

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)


153
154
155
156
157
158
159
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 153

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



578
579
580
581
582
583
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 578

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



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 125

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



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 345

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



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 298

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) ⇒ Object



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

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



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

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

.run_yarn_commands(*commands) ⇒ Object



279
280
281
282
283
284
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 279

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



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 252

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)


247
248
249
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 247

def self.yarn_4_or_higher?
  yarn_major_version >= 4
end

.yarn_berry?(yarn_lock) ⇒ Boolean

Returns:

  • (Boolean)


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

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



224
225
226
227
228
229
230
231
232
233
234
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 224

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)


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

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

.yarn_berry_skip_build?Boolean

Returns:

  • (Boolean)


237
238
239
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 237

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

.yarn_major_versionObject



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 172

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)


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

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



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

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)


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

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