NpmPackageJsonBuildFileUpdater.kt
package com.depanalyzer.update
import com.depanalyzer.security.InputSafety
import tools.jackson.databind.json.JsonMapper
import tools.jackson.databind.node.ObjectNode
import java.io.File
class NpmPackageJsonBuildFileUpdater : BuildFileUpdater {
private val jsonMapper = JsonMapper.builder().build()
override fun applyUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
if (!InputSafety.isSafeVersion(suggestion.newVersion)) return false
if (!buildFile.exists() || !buildFile.isFile || buildFile.name != "package.json") return false
val root = runCatching { jsonMapper.readTree(buildFile) as? ObjectNode }.getOrNull() ?: return false
val packageName = if (suggestion.groupId == "npm") {
suggestion.artifactId
} else {
"${suggestion.groupId}/${suggestion.artifactId}"
}
val updated = when (suggestion.targetType) {
UpdateTargetType.DIRECT -> updateDirect(root, packageName, suggestion.newVersion)
UpdateTargetType.TRANSITIVE_OVERRIDE -> updateOverride(root, packageName, suggestion.newVersion)
}
if (!updated) return false
val pretty = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(root) + "\n"
buildFile.writeText(pretty)
return true
}
private fun updateDirect(root: ObjectNode, packageName: String, newVersion: String): Boolean {
val sections = listOf("dependencies", "devDependencies", "peerDependencies", "optionalDependencies")
sections.forEach { section ->
val node = root.path(section)
if (node is ObjectNode && node.has(packageName)) {
val current = node.path(packageName).textOrEmpty()
val replacement = preservePrefix(current, newVersion)
node.put(packageName, replacement)
return true
}
}
return false
}
private fun updateOverride(root: ObjectNode, packageName: String, newVersion: String): Boolean {
val overrides = when (val current = root.path("overrides")) {
is ObjectNode -> current
else -> root.putObject("overrides")
}
val existing = overrides.path(packageName).textOrEmpty()
if (existing == newVersion) return false
overrides.put(packageName, newVersion)
return true
}
private fun preservePrefix(currentSpec: String, newVersion: String): String {
val trimmed = currentSpec.trim()
return when {
trimmed.startsWith("^") -> "^$newVersion"
trimmed.startsWith("~") -> "~$newVersion"
trimmed.startsWith(">=") -> ">=$newVersion"
trimmed.startsWith("<=") -> "<=$newVersion"
trimmed.startsWith(">") -> ">$newVersion"
trimmed.startsWith("<") -> "<$newVersion"
trimmed.startsWith("=") -> "=$newVersion"
else -> newVersion
}
}
private fun tools.jackson.databind.JsonNode.textOrEmpty(): String = scalarText().trim()
private fun tools.jackson.databind.JsonNode.scalarText(): String = when {
isNull || isMissingNode -> ""
else -> toString().removeSurrounding("\"")
}
}