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)/

Class Method Summary collapse

Class Method Details

.dependencies_with_all_versions_metadata(dependency_set) ⇒ Object



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

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



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

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



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 103

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)


61
62
63
64
65
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 61

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

  npm_version_numeric(package_lock) == 8
end

.npm_version_numeric(lockfile) ⇒ Object



18
19
20
21
22
23
24
25
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 18

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

  6
rescue JSON::ParserError
  6
end

.pnpm_lockfile_version(pnpm_lock) ⇒ Object



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

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

.pnpm_version_numeric(pnpm_lock) ⇒ Object



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

def self.pnpm_version_numeric(pnpm_lock)
  if pnpm_lockfile_version(pnpm_lock).to_f >= 9.0
    9
  elsif pnpm_lockfile_version(pnpm_lock).to_f >= 6.0
    8
  elsif pnpm_lockfile_version(pnpm_lock).to_f >= 5.4
    7
  else
    6
  end
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.



192
193
194
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 192

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



203
204
205
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 203

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



197
198
199
200
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 197

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.



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

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

.setup_yarn_berryObject



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 156

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)


152
153
154
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 152

def self.yarn_4_or_higher?
  yarn_major_version >= 4
end

.yarn_berry?(yarn_lock) ⇒ Boolean

Returns:

  • (Boolean)


68
69
70
71
72
73
74
75
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 68

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



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 129

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)


147
148
149
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 147

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

.yarn_berry_skip_build?Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 142

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

.yarn_major_versionObject



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 78

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)


123
124
125
126
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 123

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



28
29
30
31
32
33
34
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 28

def self.yarn_version_numeric(yarn_lock)
  if yarn_berry?(yarn_lock)
    3
  else
    1
  end
end

.yarn_zero_install?Boolean

Returns:

  • (Boolean)


118
119
120
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 118

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