Skip to content

Commit

Permalink
Add support for packageManager tag updates
Browse files Browse the repository at this point in the history
  • Loading branch information
WarningImHack3r committed Sep 28, 2024
1 parent e73a7a8 commit b431026
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

### Added

- **Add support for updating the `packageManager` tag**
- Add HTTP caching for some requests to the npm registry, improving performance (#135)
- Attempt to batch registries scan without network requests before falling back to the original behavior, speeding up the first scan (#136)
- Many thanks to [@SCjona](https://github.com/SCjona) for the whole 3.1.0 and the two issues of this release!
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.github.warningimhack3r.npmupdatedependencies
pluginName = npm-update-dependencies
pluginRepositoryUrl = https://github.com/WarningImHack3r/npm-update-dependencies
# SemVer format -> https://semver.org
pluginVersion = 3.1.0
pluginVersion = 3.2.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 223
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,14 @@ class NUDState {
field = value
StatusBarHelper.updateWidget()
}
var isScanningForPackageManager = false
set(value) {
field = value
StatusBarHelper.updateWidget()
}
var foundPackageManager: String? = null
set(value) {
field = value
StatusBarHelper.updateWidget()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.github.warningimhack3r.npmupdatedependencies.ui.annotation

import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NUDState
import com.github.warningimhack3r.npmupdatedependencies.backend.engine.RegistriesScanner
import com.github.warningimhack3r.npmupdatedependencies.backend.engine.checkers.PackageUpdateChecker
import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.stringValue
import com.github.warningimhack3r.npmupdatedependencies.backend.models.DataState
import com.github.warningimhack3r.npmupdatedependencies.backend.models.Property
import com.github.warningimhack3r.npmupdatedependencies.backend.models.Update
import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.BlacklistVersionFix
import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.UpdatePackageManagerFix
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.json.psi.JsonProperty
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.ExternalAnnotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.applyIf
import kotlinx.datetime.Clock
import org.semver4j.Semver

class PackageManagerAnnotator : DumbAware, ExternalAnnotator<
Pair<Project, Property>,
Pair<JsonProperty, Update>
>() {
companion object {
private val log = logger<PackageManagerAnnotator>()
}

override fun collectInformation(file: PsiFile, editor: Editor, hasErrors: Boolean): Pair<Project, Property>? {
if (file.name != "package.json") return null
return PsiTreeUtil.findChildrenOfType(file, JsonProperty::class.java)
.first { child ->
child.name == "packageManager"
}.let { property ->
Pair(
file.project,
Property(property, property.name, property.value?.stringValue())
).also {
log.debug("Found package manager: ${it.second.comparator}")
}
}
}

override fun doAnnotate(collectedInfo: Pair<Project, Property>?): Pair<JsonProperty, Update>? {
if (collectedInfo == null) return null
val (project, property) = collectedInfo

val state = NUDState.getInstance(project)
val registriesScanner = RegistriesScanner.getInstance(project)
if (!registriesScanner.scanned && !state.isScanningForRegistries) {
log.debug("Registries not scanned yet, scanning now")
state.isScanningForRegistries = true
registriesScanner.scan()
state.isScanningForRegistries = false
log.debug("Registries scanned")
}

log.info("Starting checking for package manager update")
state.foundPackageManager = null
state.isScanningForPackageManager = true

if (property.comparator == null) return null
if (!property.comparator.contains("@") // not a valid package manager format
|| property.comparator.split("@").size != 2 // more than one @
|| property.comparator.startsWith("@") // no package manager name
|| property.comparator.endsWith("@") // no version
) {
log.warn("Invalid package manager: ${property.comparator}")
return null
}

val (managerName, managerVersion) = property.comparator.split("@")
val update = PackageUpdateChecker.getInstance(project)
.checkAvailableUpdates(managerName, "^${managerVersion}")
state.availableUpdates[managerName] = state.availableUpdates[managerName].let { currentState ->
if (currentState == null || currentState.data != update) DataState(
data = update,
addedAt = Clock.System.now(),
comparator = managerVersion
) else currentState
}
state.isScanningForPackageManager = false
if (update == null) {
log.debug("No update found for $managerName")
return null
}
log.info("Found update for $managerName: $update")
state.foundPackageManager = managerName
return Pair(property.jsonProperty, update)
}

override fun apply(file: PsiFile, annotationResult: Pair<JsonProperty, Update>?, holder: AnnotationHolder) {
if (annotationResult == null) return
val (property, update) = annotationResult
log.debug("Applying annotation for ${property.name}")
val message = "An update is available!" + if (update.affectedByFilters.isNotEmpty()) {
" (The following filters affected the result: ${update.affectedByFilters.joinToString(", ")})"
} else ""
val (managerName, managerVersion) = property.value?.stringValue()?.split("@") ?: return
val currentVersion = Semver.coerce(managerVersion)
holder.newAnnotation(HighlightSeverity.WARNING, message)
.range(property.value!!.textRange)
.highlightType(ProblemHighlightType.WARNING)
.withFix(UpdatePackageManagerFix(property, update))
.applyIf(currentVersion != null) {
if (currentVersion == null) return@applyIf this

withFix(
BlacklistVersionFix(
-1, managerName,
"${currentVersion.major + 1}.x.x"
)
)
withFix(
BlacklistVersionFix(
0, managerName,
"${currentVersion.major}.${currentVersion.minor + 1}.x"
)
)
withFix(
BlacklistVersionFix(
1, managerName,
"${currentVersion.major}.${currentVersion.minor}.${currentVersion.patch + 1}"
)
)
withFix(
BlacklistVersionFix(
2, managerName,
"*", "ALL versions"
)
)
}
.needsUpdateOnTyping()
.create()

log.debug("Annotation applied for ${property.name}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.warningimhack3r.npmupdatedependencies.ui.quickfix

import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.stringValue
import com.github.warningimhack3r.npmupdatedependencies.backend.models.Update
import com.github.warningimhack3r.npmupdatedependencies.ui.helpers.NUDHelper
import com.github.warningimhack3r.npmupdatedependencies.ui.helpers.QuickFixesCommon
import com.intellij.codeInsight.intention.impl.BaseIntentionAction
import com.intellij.json.psi.JsonProperty
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile

class UpdatePackageManagerFix(
private val property: JsonProperty,
update: Update
) : BaseIntentionAction() {
companion object {
private val log = logger<UpdatePackageManagerFix>()
}

private val packageManager = property.value?.stringValue()?.substringBefore("@")
private val targetVersion = update.versions.latest

override fun getText() = "1. Update $packageManager to $targetVersion"

override fun getFamilyName() = "Update package manager"

override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?) =
QuickFixesCommon.getAvailability(editor, file)

override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (file == null) {
log.warn("Trying to update package manager but file is null")
return
}
val newElement = NUDHelper.createElement(project, "\"$packageManager@${targetVersion}\"", "JSON")
NUDHelper.safeFileWrite(file, "Update $packageManager to $targetVersion") {
property.value?.replace(newElement)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget.
SCANNING_PACKAGES,
SCANNING_FOR_UPDATES,
SCANNING_FOR_DEPRECATIONS,
SCANNING_FOR_PACKAGE_MANAGER,
READY
}

Expand All @@ -70,6 +71,7 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget.
Status.SCANNING_PACKAGES -> "Scanning packages..."
Status.SCANNING_FOR_UPDATES -> "Scanning for updates..."
Status.SCANNING_FOR_DEPRECATIONS -> "Scanning for deprecations..."
Status.SCANNING_FOR_PACKAGE_MANAGER -> "Scanning for package manager updates..."
Status.READY -> "Click to see available updates"
}

Expand Down Expand Up @@ -102,7 +104,7 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget.
// Find the line number of the dependency
val lineNumber = document?.let {
it.text.split("\n").indexOfFirst { line ->
line.contains("\"$dependencyName\":")
line.contains("\"$dependencyName\":") || line.contains("\"$dependencyName@")
}
} ?: 0
// Find the column number at the end of the dependency line
Expand All @@ -111,28 +113,41 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget.
char == ','
}?.plus(1)
} ?: 0
if (lineNumber == 0) return
// Open the file
OpenFileDescriptor(project, file, lineNumber, columnNumber).navigate(true)
}
}

val state = NUDState.getInstance(project)
return JBPopupFactory.getInstance().createActionGroupPopup(
"Available Changes",
DefaultActionGroup().apply {
if (state.foundPackageManager != null) {
addSeparator("Package Manager")
add(DumbAwareAction.create(state.foundPackageManager) {
val packageManagerName = state.foundPackageManager
if (packageManagerName != null) {
openPackageJson(packageManagerName)
}
})
}
addSeparator("Updates")
addAll(
NUDState.getInstance(project).availableUpdates.filter { it.value.data != null }.toSortedMap()
.map { update ->
DumbAwareAction.create(update.key) {
openPackageJson(update.key)
state.availableUpdates.filter {
it.value.data != null && it.key != state.foundPackageManager
}.toSortedMap()
.map { (key, _) ->
DumbAwareAction.create(key) {
openPackageJson(key)
}
})
addSeparator("Deprecations")
addAll(
NUDState.getInstance(project).deprecations.filter { it.value.data != null }.toSortedMap()
.map { deprecation ->
DumbAwareAction.create(deprecation.key) {
openPackageJson(deprecation.key)
state.deprecations.filter { it.value.data != null }.toSortedMap()
.map { (key, _) ->
DumbAwareAction.create(key) {
openPackageJson(key)
}
})
},
Expand All @@ -151,8 +166,10 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget.
Status.SCANNING_PACKAGES -> "Scanning packages (${state.scannedUpdates + state.scannedDeprecations}/${state.totalPackages * 2})..."
Status.SCANNING_FOR_UPDATES -> "Scanning for updates (${state.scannedUpdates}/${state.totalPackages})..."
Status.SCANNING_FOR_DEPRECATIONS -> "Scanning for deprecations (${state.scannedDeprecations}/${state.totalPackages})..."
Status.SCANNING_FOR_PACKAGE_MANAGER -> "Scanning for package manager updates..."
Status.READY -> {
val outdated = state.availableUpdates.filter { it.value.data != null }.size
val outdated =
state.availableUpdates.filter { it.value.data != null }.size + if (state.foundPackageManager != null) 1 else 0
val deprecated = state.deprecations.filter { it.value.data != null }.size
when (NUDSettingsState.instance.statusBarMode) {
StatusBarMode.FULL -> when {
Expand Down Expand Up @@ -184,6 +201,7 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget.
state.isScanningForUpdates && state.isScanningForDeprecations -> Status.SCANNING_PACKAGES
state.isScanningForUpdates -> Status.SCANNING_FOR_UPDATES
state.isScanningForDeprecations -> Status.SCANNING_FOR_DEPRECATIONS
state.isScanningForPackageManager -> Status.SCANNING_FOR_PACKAGE_MANAGER
else -> Status.READY
}
myStatusBar.updateWidget(ID())
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
<externalAnnotator
language="JSON"
implementationClass="com.github.warningimhack3r.npmupdatedependencies.ui.annotation.UpdatesAnnotator"/>
<externalAnnotator
language="JSON"
implementationClass="com.github.warningimhack3r.npmupdatedependencies.ui.annotation.PackageManagerAnnotator"/>
<statusBarWidgetFactory
id="NpmUpdateDependenciesStatusBarEditorFactory"
implementation="com.github.warningimhack3r.npmupdatedependencies.ui.statusbar.StatusBarFactory"/>
Expand Down

0 comments on commit b431026

Please sign in to comment.