diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddb8ec..dabd9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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! diff --git a/gradle.properties b/gradle.properties index f2c7eeb..bdb71dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NUDState.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NUDState.kt index 93f316a..df124d9 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NUDState.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NUDState.kt @@ -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() + } } diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/PackageManagerAnnotator.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/PackageManagerAnnotator.kt new file mode 100644 index 0000000..9210196 --- /dev/null +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/PackageManagerAnnotator.kt @@ -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, + Pair + >() { + companion object { + private val log = logger() + } + + override fun collectInformation(file: PsiFile, editor: Editor, hasErrors: Boolean): Pair? { + 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?): Pair? { + 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?, 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}") + } +} diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/quickfix/UpdatePackageManagerFix.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/quickfix/UpdatePackageManagerFix.kt new file mode 100644 index 0000000..4cc409d --- /dev/null +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/quickfix/UpdatePackageManagerFix.kt @@ -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() + } + + 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) + } + } +} diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarFactory.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarFactory.kt index 5ad5a8e..923e093 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarFactory.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarFactory.kt @@ -55,6 +55,7 @@ class WidgetBar(project: Project) : EditorBasedWidget(project), StatusBarWidget. SCANNING_PACKAGES, SCANNING_FOR_UPDATES, SCANNING_FOR_DEPRECATIONS, + SCANNING_FOR_PACKAGE_MANAGER, READY } @@ -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" } @@ -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 @@ -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) } }) }, @@ -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 { @@ -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()) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 28f204e..51464ea 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -105,6 +105,9 @@ +