Skip to content

Commit ad41e5f

Browse files
committed
refactor(yarn2): Extract the command into a separate class
This is required as a preparation for migrating package managers to the new plugin API. Signed-off-by: Martin Nonnenmacher <[email protected]>
1 parent f134915 commit ad41e5f

File tree

2 files changed

+46
-48
lines changed

2 files changed

+46
-48
lines changed

plugins/package-managers/node/src/main/kotlin/yarn2/Yarn2.kt

+40-38
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,39 @@ private enum class YarnDependencyType(val type: String) {
8989
DEV_DEPENDENCIES("devDependencies")
9090
}
9191

92+
internal class Yarn2Command(private val enableCorepack: Boolean?) : CommandLineTool {
93+
@Suppress("Unused") // The no-arg constructor is required by the requirements command.
94+
constructor() : this(null)
95+
96+
/**
97+
* The Yarn 2+ executable is not installed globally: The program shipped by the project in `.yarn/releases` is used
98+
* instead. The value of the 'yarnPath' property in the resource file `.yarnrc.yml` defines the path to the
99+
* executable for the current project e.g. `yarnPath: .yarn/releases/yarn-3.2.1.cjs`.
100+
* This map holds the mapping between the directory and their Yarn 2+ executables. It is only used if Yarn has not
101+
* been installed via Corepack; then it is accessed under a default name.
102+
*/
103+
private val yarn2ExecutablesByPath: MutableMap<File, File> = mutableMapOf()
104+
105+
override fun command(workingDir: File?): String {
106+
if (workingDir == null) return ""
107+
if (isCorepackEnabled(workingDir)) return "yarn"
108+
109+
val executablePath = yarn2ExecutablesByPath.getOrPut(workingDir) { getYarnExecutable(workingDir) }.absolutePath
110+
return executablePath.takeUnless { Os.isWindows } ?: "node $executablePath"
111+
}
112+
113+
override fun getVersion(workingDir: File?): String =
114+
// `getVersion` with a `null` parameter is called once by the Analyzer to get the version of the global tool.
115+
// For Yarn2+, the version is specific to each definition file being scanned therefore a global version doesn't
116+
// apply.
117+
// TODO: An alternative would be to collate the versions of all tools in `yarn2CommandsByPath`.
118+
if (workingDir == null) "" else super.getVersion(workingDir)
119+
120+
override fun getVersionRequirement(): RangesList = RangesListFactory.create(">=2.0.0")
121+
122+
private fun isCorepackEnabled(workingDir: File): Boolean = enableCorepack ?: isCorepackEnabledInManifest(workingDir)
123+
}
124+
92125
/**
93126
* The [Yarn 2+](https://v2.yarnpkg.com/) package manager for JavaScript.
94127
*
@@ -102,7 +135,7 @@ private enum class YarnDependencyType(val type: String) {
102135
* force a specific behavior in some environments.
103136
*/
104137
class Yarn2(name: String, analyzerConfig: AnalyzerConfiguration) :
105-
NodePackageManager(name, NodePackageManagerType.YARN2, analyzerConfig), CommandLineTool {
138+
NodePackageManager(name, NodePackageManagerType.YARN2, analyzerConfig) {
106139
companion object {
107140
/**
108141
* The name of the option to disable HTTPS server certificate verification.
@@ -121,15 +154,6 @@ class Yarn2(name: String, analyzerConfig: AnalyzerConfiguration) :
121154

122155
override val globsForDefinitionFiles = listOf(NodePackageManagerType.DEFINITION_FILE)
123156

124-
/**
125-
* The Yarn 2+ executable is not installed globally: The program shipped by the project in `.yarn/releases` is used
126-
* instead. The value of the 'yarnPath' property in the resource file `.yarnrc.yml` defines the path to the
127-
* executable for the current project e.g. `yarnPath: .yarn/releases/yarn-3.2.1.cjs`.
128-
* This map holds the mapping between the directory and their Yarn 2+ executables. It is only used if Yarn has not
129-
* been installed via Corepack; then it is accessed under a default name.
130-
*/
131-
private val yarn2ExecutablesByPath: MutableMap<File, File> = mutableMapOf()
132-
133157
private val disableRegistryCertificateVerification =
134158
options[OPTION_DISABLE_REGISTRY_CERTIFICATE_VERIFICATION].toBoolean()
135159

@@ -139,38 +163,16 @@ class Yarn2(name: String, analyzerConfig: AnalyzerConfiguration) :
139163
// All the projects parsed by this package manager, mapped by their ids.
140164
private val allProjects = mutableMapOf<Identifier, Project>()
141165

166+
internal val yarn2Command = Yarn2Command(options[OPTION_COREPACK_OVERRIDE].toBoolean())
167+
142168
// The issues that have been found when resolving the dependencies.
143169
private val issues = mutableListOf<Issue>()
144170

145171
// A builder to build the dependency graph of the project.
146172
override val graphBuilder = DependencyGraphBuilder(Yarn2DependencyHandler())
147173

148-
override fun command(workingDir: File?): String {
149-
if (workingDir == null) return ""
150-
if (isCorepackEnabled(workingDir)) return "yarn"
151-
152-
val executablePath = yarn2ExecutablesByPath.getOrPut(workingDir) { getYarnExecutable(workingDir) }.absolutePath
153-
return executablePath.takeUnless { Os.isWindows } ?: "node $executablePath"
154-
}
155-
156-
override fun getVersion(workingDir: File?): String =
157-
// `getVersion` with a `null` parameter is called once by the Analyzer to get the version of the global tool.
158-
// For Yarn2+, the version is specific to each definition file being scanned therefore a global version doesn't
159-
// apply.
160-
// TODO: An alternative would be to collate the versions of all tools in `yarn2CommandsByPath`.
161-
if (workingDir == null) "" else super.getVersion(workingDir)
162-
163-
override fun getVersionRequirement(): RangesList = RangesListFactory.create(">=2.0.0")
164-
165-
private fun isCorepackEnabled(workingDir: File): Boolean =
166-
if (OPTION_COREPACK_OVERRIDE in options) {
167-
options[OPTION_COREPACK_OVERRIDE].toBoolean()
168-
} else {
169-
isCorepackEnabledInManifest(workingDir)
170-
}
171-
172174
override fun beforeResolution(analysisRoot: File, definitionFiles: List<File>) =
173-
definitionFiles.forEach { checkVersion(it.parentFile) }
175+
definitionFiles.forEach { yarn2Command.checkVersion(it.parentFile) }
174176

175177
override fun resolveDependencies(
176178
analysisRoot: File,
@@ -197,11 +199,11 @@ class Yarn2(name: String, analyzerConfig: AnalyzerConfiguration) :
197199
}
198200

199201
private fun installDependencies(workingDir: File) {
200-
run("install", workingDir = workingDir).requireSuccess()
202+
yarn2Command.run("install", workingDir = workingDir).requireSuccess()
201203
}
202204

203205
private fun getPackageInfos(workingDir: File): List<PackageInfo> {
204-
val process = run(
206+
val process = yarn2Command.run(
205207
"info",
206208
"-A",
207209
"-R",
@@ -256,7 +258,7 @@ class Yarn2(name: String, analyzerConfig: AnalyzerConfiguration) :
256258
async {
257259
logger.info { "Fetching packages details chunk #$index." }
258260

259-
val process = run(
261+
val process = yarn2Command.run(
260262
"npm",
261263
"info",
262264
"--json",

plugins/package-managers/node/src/test/kotlin/yarn2/Yarn2Test.kt

+6-10
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ class Yarn2Test : WordSpec({
5555
val workingDir = tempdir()
5656
workingDir.resolve(".yarnrc.yml").writeText("someProperty: some-value")
5757

58-
val yarn = Yarn2("yarn", AnalyzerConfiguration())
59-
6058
val exception = shouldThrow<IllegalArgumentException> {
61-
yarn.command(workingDir)
59+
Yarn2Command().command(workingDir)
6260
}
6361

6462
exception.localizedMessage shouldContain "No Yarn 2+ executable"
@@ -69,24 +67,23 @@ class Yarn2Test : WordSpec({
6967
val executable = "non-existing-yarn-wrapper.js"
7068
workingDir.resolve(".yarnrc.yml").writeText("yarnPath: $executable")
7169

72-
val yarn = Yarn2("yarn", AnalyzerConfiguration())
73-
7470
val exception = shouldThrow<IllegalArgumentException> {
75-
yarn.command(workingDir)
71+
Yarn2Command().command(workingDir)
7672
}
7773

7874
exception.localizedMessage shouldContain executable
7975
}
8076

8177
"return the default executable name if Corepack is enabled based on the configuration option" {
8278
val workingDir = tempdir()
79+
8380
val yarn2Options = mapOf("corepackOverride" to "true")
8481
val analyzerConfiguration = AnalyzerConfiguration(
8582
packageManagers = mapOf("Yarn2" to PackageManagerConfiguration(options = yarn2Options))
8683
)
8784

8885
val yarn = Yarn2("Yarn2", analyzerConfiguration)
89-
val command = yarn.command(workingDir)
86+
val command = yarn.yarn2Command.command(workingDir)
9087

9188
command shouldBe "yarn"
9289
}
@@ -95,8 +92,7 @@ class Yarn2Test : WordSpec({
9592
val workingDir = tempdir()
9693
writePackageJson(workingDir, "[email protected]")
9794

98-
val yarn = Yarn2("Yarn2", AnalyzerConfiguration())
99-
val command = yarn.command(workingDir)
95+
val command = Yarn2Command().command(workingDir)
10096

10197
command shouldBe "yarn"
10298
}
@@ -129,7 +125,7 @@ private fun checkExecutableFromYarnRc(workingDir: File, config: AnalyzerConfigur
129125
}
130126

131127
val yarn = Yarn2("Yarn2", config)
132-
val command = yarn.command(workingDir)
128+
val command = yarn.yarn2Command.command(workingDir)
133129

134130
if (Os.isWindows) {
135131
command shouldBe "node ${executableFile.absolutePath}"

0 commit comments

Comments
 (0)