Skip to content

Commit

Permalink
0.7.3
Browse files Browse the repository at this point in the history
- Notify of useless components files on update
- Allow removing useless installed deps on uninstall
- Avoid fetching source and installed components twice
- Improve asynchronous handling
- Fix executed commands on UNIX
- Filter back out node_modules (+ .git) from file finder
- Remove logging on exceptions
  • Loading branch information
WarningImHack3r committed Mar 2, 2024
1 parent 5984d04 commit 0e59e0e
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 144 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@

## [Unreleased]

## [0.7.3] - 2024-03-02

### Added

- [Svelte/Vue] Notify when an updated component does no longer uses some installed files, allowing to remove them
- Allow to remove installed dependencies when they are no longer used by any component

### Fixed

- Fix a freeze when executing commands on macOS/Linux
- Improve performance and stability

## [0.7.2] - 2024-02-16

### Fixed
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.intellijshadcnplugin
pluginName = intellij-shadcn-plugin
pluginRepositoryUrl = https://github.com/WarningImHack3r/intellij-shadcn-plugin
# SemVer format -> https://semver.org
pluginVersion = 0.7.2
pluginVersion = 0.7.3

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 213
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,78 @@ class DependencyManager(private val project: Project) {
PROD
}

private fun installDependency(dependencyName: String, installationType: InstallationType = InstallationType.PROD) {
// get the package manager
private fun getPackageManager(): String? {
val fileManager = FileManager(project)
val packageManager = mapOf(
return mapOf(
"package-lock.json" to "npm",
"pnpm-lock.yaml" to "pnpm",
"yarn.lock" to "yarn",
"bun.lockb" to "bun"
).filter { runReadAction {
fileManager.getVirtualFilesByName(it.key).isNotEmpty()
} }.values.firstOrNull()
// install the dependency
val command = listOfNotNull(
packageManager,
"i",
if (installationType == InstallationType.DEV) "-D" else null,
dependencyName
).toTypedArray()
val res = ShellRunner(project).execute(command)
// check if the installation was successful
if (res == null) {
NotificationManager(project).sendNotification(
"Failed to install dependency $dependencyName",
"Failed to install dependency $dependencyName (${command.joinToString(" ")}). Please install it manually.",
NotificationType.ERROR
)
}
).filter {
runReadAction {
fileManager.getVirtualFilesByName(it.key).isNotEmpty()
}
}.values.firstOrNull()
}

fun installDependencies(dependencyNames: List<String>, installationType: InstallationType = InstallationType.PROD) {
dependencyNames.forEach { installDependency(it, installationType) }
getPackageManager()?.let { packageManager ->
// install the dependency
val command = listOfNotNull(
packageManager,
"i",
if (installationType == InstallationType.DEV) "-D" else null,
*dependencyNames.toTypedArray()
).toTypedArray()
val res = ShellRunner(project).execute(command)
// check if the installation was successful
if (res == null) {
NotificationManager(project).sendNotification(
"Failed to install dependencies",
"Failed to install dependencies: ${dependencyNames.joinToString { ", " }} (${command.joinToString(" ")}). Please install it manually.",
NotificationType.ERROR
)
}
} ?: throw IllegalStateException("No package manager found")
}

fun isDependencyInstalled(dependency: String): Boolean {
fun uninstallDependencies(dependencyNames: List<String>) {
getPackageManager()?.let { packageManager ->
// uninstall the dependencies
val command = listOf(
packageManager,
"remove",
*dependencyNames.toTypedArray()
).toTypedArray()
val res = ShellRunner(project).execute(command)
// check if the uninstallation was successful
if (res == null) {
NotificationManager(project).sendNotification(
"Failed to uninstall dependencies",
"Failed to uninstall dependencies (${command.joinToString(" ")}). Please uninstall them manually.",
NotificationType.ERROR
)
}
} ?: throw IllegalStateException("No package manager found")
}

fun getInstalledDependencies(): List<String> {
// Read the package.json file
return FileManager(project).getFileContentsAtPath("package.json")?.let { packageJson ->
Json.parseToJsonElement(packageJson).jsonObject.filter {
it.key == "dependencies" || it.key == "devDependencies"
}.map { it.value.jsonObject.keys }.flatten().also {
logger<DependencyManager>().debug("Installed dependencies: $it, is $dependency installed? ${it.contains(dependency)}")
}.contains(dependency)
// Check if the dependency is installed
} ?: false.also {
logger<DependencyManager>().error("package.json not found, returning false")
logger<DependencyManager>().debug("Installed dependencies: $it")
}
} ?: emptyList<String>().also {
logger<DependencyManager>().error("package.json not found")
}
}

fun isDependencyInstalled(dependency: String): Boolean {
// Read the package.json file
return getInstalledDependencies().contains(dependency).also {
logger<DependencyManager>().debug("Is $dependency installed? $it")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ class FileManager(private val project: Project) {
}
}

fun deleteFileAtPath(path: String): Boolean {
fun deleteFile(file: VirtualFile): Boolean {
return try {
getFileAtPath(path)?.delete(this)?.let { true } ?: false
file.delete(this).let { true }
} catch (e: IOException) {
false
}.also {
if (!it) log.warn("Unable to delete file at path $path")
else log.debug("Deleted file at path $path")
if (!it) log.warn("Unable to delete file at path ${file.path}")
else log.debug("Deleted file at path ${file.path}")
}
}

fun deleteFileAtPath(path: String): Boolean {
return getFileAtPath(path)?.let { deleteFile(it) } ?: false.also {
log.warn("No file to delete found at path $path")
}
}

Expand Down Expand Up @@ -60,7 +66,9 @@ class FileManager(private val project: Project) {
name,
GlobalSearchScope.projectScope(project)
)
}).sortedBy { file ->
}).filter { file ->
!file.path.contains("/node_modules/") && !file.path.contains("/.git/")
}.sortedBy { file ->
name.toRegex().find(file.path)?.range?.first ?: Int.MAX_VALUE
}.also {
log.debug("Found ${it.size} files named $name: ${it.toList()}")
Expand All @@ -69,9 +77,7 @@ class FileManager(private val project: Project) {

private fun getDeepestFileForPath(filePath: String): VirtualFile {
var paths = filePath.split('/')
var currentFile = getVirtualFilesByName(paths.first()).firstOrNull() ?: throw NoSuchFileException("No file found at path $filePath").also {
log.warn("No file found at path ${paths.first()}")
}
var currentFile = getVirtualFilesByName(paths.first()).firstOrNull() ?: throw NoSuchFileException("No file found at path $filePath")
paths = paths.drop(1)
for (path in paths) {
val child = currentFile.findChild(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ class ShellRunner(private val project: Project? = null) {
val platformCommand = if (isWindows()) {
arrayOf("cmd", "/c")
} else {
arrayOf("sh", "-c")
emptyArray()
} + command
log.debug("Executing command: \"${platformCommand.joinToString(" ")}\"")
val process = ProcessBuilder(*platformCommand)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.directory(project?.basePath?.let { File(it) })
Expand All @@ -33,6 +34,7 @@ class ShellRunner(private val project: Project? = null) {
}
} catch (e: Exception) {
if (isWindows() && !commandName.endsWith(".cmd")) {
log.warn("Failed to execute \"${command.joinToString(" ")}\". Trying to execute \"$commandName.cmd\" instead", e)
failedCommands.add(commandName)
return execute(arrayOf("$commandName.cmd") + command.drop(1).toTypedArray())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.Co
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.ComponentWithContents
import com.github.warningimhack3r.intellijshadcnplugin.notifications.NotificationManager
import com.intellij.notification.NotificationAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -61,18 +62,14 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
private fun fetchComponent(componentName: String): ComponentWithContents {
val style = getLocalConfig().style
val response = RequestSender.sendRequest("$domain/registry/styles/$style/$componentName.json")
return response.ok { Json.decodeFromString(it.body) } ?: throw Exception("Component not found").also {
log.error("Unable to fetch component $componentName", it)
}
return response.ok { Json.decodeFromString(it.body) } ?: throw Exception("Component $componentName not found")
}

protected fun fetchColors(): JsonElement {
val baseColor = getLocalConfig().tailwind.baseColor
return RequestSender.sendRequest("$domain/registry/colors/$baseColor.json").ok {
Json.parseToJsonElement(it.body)
} ?: throw Exception("Colors not found").also {
log.error("Unable to fetch colors", it)
}
} ?: throw Exception("Colors not found")
}

protected open fun getRegistryDependencies(component: ComponentWithContents): List<ComponentWithContents> {
Expand All @@ -83,12 +80,12 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}

// Public methods
open fun fetchAllComponents(): List<ISPComponent> {
open fun fetchAllComponents(): List<Component> {
return RequestSender.sendRequest("$domain/registry/index.json").ok {
Json.decodeFromString<List<Component>>(it.body)
}?.map { ISPComponent(it.name) }?.also {
}?.also {
log.info("Fetched ${it.size} remote components: ${it.joinToString(", ") { component -> component.name }}")
} ?: emptyList<ISPComponent>().also {
} ?: emptyList<Component>().also {
log.error("Unable to fetch remote components")
}
}
Expand All @@ -109,12 +106,55 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
// Install component
val component = fetchComponent(componentName)
val installedComponents = getInstalledComponents()
val fileManager = FileManager(project)
val notifManager = NotificationManager(project)
log.debug("Installing ${component.name} (installed: ${installedComponents.joinToString(", ")})")
setOf(component, *getRegistryDependencies(component).filter {
!installedComponents.contains(it.name)
}.toTypedArray<ComponentWithContents>()).also {
log.debug("Installing ${it.size} components: ${it.joinToString(", ") { component -> component.name }}")
}.forEach { downloadedComponent ->
val path =
"${resolveAlias(getLocalConfig().aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) {
"/${downloadedComponent.name}"
} else ""
if (usesDirectoriesForComponents()) {
val remotelyDeletedFiles = fileManager.getFileAtPath(path)?.children?.filter { file ->
downloadedComponent.files.none { it.name == file.name }
} ?: emptyList()
val multipleFiles = remotelyDeletedFiles.size > 1
notifManager.sendNotification(
"Deprecated component file${if (multipleFiles) "s" else ""} in ${downloadedComponent.name}",
"The following file${if (multipleFiles) "s are" else " is"} no longer part of ${downloadedComponent.name}: ${
remotelyDeletedFiles.joinToString(", ") { file ->
file.name
}
}. Do you want to remove ${if (multipleFiles) "them" else "it"}?"
) { notification ->
listOf(
NotificationAction.createSimple("Remove " + if (multipleFiles) "them" else "it") {
remotelyDeletedFiles.forEach { file ->
runWriteAction { fileManager.deleteFile(file) }
}
log.info(
"Removed deprecated file${if (multipleFiles) "s" else ""} from ${downloadedComponent.name} (${
downloadedComponent.files.joinToString(", ") { file ->
file.name
}
}): ${
remotelyDeletedFiles.joinToString(", ") { file ->
file.name
}
}"
)
notification.expire()
},
NotificationAction.createSimple("Keep " + if (multipleFiles) "them" else "it") {
notification.expire()
}
)
}
}
downloadedComponent.files.forEach { file ->
val psiFile = PsiFileFactory.getInstance(project).createFileFromText(
adaptFileExtensionToConfig(file.name),
Expand All @@ -123,10 +163,7 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
),
adaptFileToConfig(file.content)
)
val path = "${resolveAlias(getLocalConfig().aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) {
"/${downloadedComponent.name}"
} else ""
FileManager(project).saveFileAtPath(psiFile, path)
fileManager.saveFileAtPath(psiFile, path)
}
}

Expand All @@ -142,7 +179,6 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
"${dropLast(1).joinToString(", ")} and ${last()}"
}
}
val notifManager = NotificationManager(project)
notifManager.sendNotification(
"Installed ${component.name}",
"${component.name} requires $dependenciesList to be installed."
Expand Down Expand Up @@ -170,9 +206,11 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
val remoteComponent = fetchComponent(componentName)
return remoteComponent.files.all { file ->
(FileManager(project).getFileContentsAtPath(
"${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${if (usesDirectoriesForComponents()) {
"/${remoteComponent.name}"
} else ""}/${file.name}"
"${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${
if (usesDirectoriesForComponents()) {
"/${remoteComponent.name}"
} else ""
}/${file.name}"
) == adaptFileToConfig(file.content)).also {
log.debug("File ${file.name} for ${remoteComponent.name} is ${if (it) "" else "NOT "}up to date")
}
Expand All @@ -181,13 +219,49 @@ abstract class Source<C : Config>(val project: Project, private val serializer:

open fun removeComponent(componentName: String) {
val remoteComponent = fetchComponent(componentName)
val componentsDir = "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}"
val componentsDir =
"${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}"
if (usesDirectoriesForComponents()) {
FileManager(project).deleteFileAtPath("$componentsDir/${remoteComponent.name}")
} else {
remoteComponent.files.forEach { file ->
FileManager(project).deleteFileAtPath("$componentsDir/${file.name}")
}
}
// Remove dependencies no longer needed by any component
val remoteComponents = fetchAllComponents()
val allPossiblyNeededDependencies = remoteComponents.map { it.dependencies }.flatten().toSet()
val currentlyNeededDependencies = getInstalledComponents().map { component ->
remoteComponents.find { it.name == component }?.dependencies ?: emptyList()
}.flatten().toSet()
val uselessDependencies = DependencyManager(project).getInstalledDependencies().filter { dependency ->
dependency in allPossiblyNeededDependencies && dependency !in currentlyNeededDependencies
}
if (uselessDependencies.isNotEmpty()) {
val multipleDependencies = uselessDependencies.size > 1
val notifManager = NotificationManager(project)
notifManager.sendNotification(
"Unused dependenc${if (multipleDependencies) "ies" else "y"} found",
"The following dependenc${if (multipleDependencies) "ies are" else "y is"} no longer needed by any component: ${
uselessDependencies.joinToString(", ") {
it
}
}. Do you want to remove ${if (multipleDependencies) "them" else "it"}?"
) { notif ->
listOf(
NotificationAction.createSimple("Remove") {
runAsync {
DependencyManager(project).uninstallDependencies(uselessDependencies)
}.then {
notifManager.sendNotificationAndHide(
"Removed dependenc${if (multipleDependencies) "ies" else "y"}",
"Removed ${uselessDependencies.joinToString(", ") { it }}."
)
}
notif.expire()
}
)
}
}
}
}
Loading

0 comments on commit 0e59e0e

Please sign in to comment.