Skip to content

Commit

Permalink
Improve shell runner, use services & other
Browse files Browse the repository at this point in the history
  • Loading branch information
WarningImHack3r committed Jun 20, 2024
1 parent f362269 commit 20e620b
Show file tree
Hide file tree
Showing 12 changed files with 70 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ object SourceScanner {
val log = logger<SourceScanner>()

fun findShadcnImplementation(project: Project): Source<*>? {
val fileManager = FileManager(project)
val fileManager = FileManager.getInstance(project)
return fileManager.getFileContentsAtPath("components.json")?.let { componentsJson ->
val contents = Json.parseToJsonElement(componentsJson).jsonObject
val schema = contents["\$schema"]?.jsonPrimitive?.content ?: ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers

import com.github.warningimhack3r.intellijshadcnplugin.notifications.NotificationManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject

@Service(Service.Level.PROJECT)
class DependencyManager(private val project: Project) {
companion object {
private val log = logger<DependencyManager>()

@JvmStatic
fun getInstance(project: Project): DependencyManager = project.service()
}

enum class InstallationType {
DEV,
PROD
}

private fun getPackageManager(): String? {
val fileManager = FileManager(project)
val fileManager = FileManager.getInstance(project)
return mapOf(
"package-lock.json" to "npm",
"pnpm-lock.yaml" to "pnpm",
Expand All @@ -34,9 +44,8 @@ class DependencyManager(private val project: Project) {
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) {
if (ShellRunner.getInstance(project).execute(command) == null) {
NotificationManager(project).sendNotification(
"Failed to install dependencies",
"Failed to install dependencies: ${dependencyNames.joinToString { ", " }} (${command.joinToString(" ")}). Please install it manually.",
Expand All @@ -54,9 +63,8 @@ class DependencyManager(private val project: Project) {
"remove",
*dependencyNames.toTypedArray()
).toTypedArray()
val res = ShellRunner(project).execute(command)
// check if the uninstallation was successful
if (res == null) {
if (ShellRunner.getInstance(project).execute(command) == null) {
NotificationManager(project).sendNotification(
"Failed to uninstall dependencies",
"Failed to uninstall dependencies (${command.joinToString(" ")}). Please uninstall them manually.",
Expand All @@ -68,14 +76,14 @@ class DependencyManager(private val project: Project) {

fun getInstalledDependencies(): List<String> {
// Read the package.json file
return FileManager(project).getFileContentsAtPath("package.json")?.let { packageJson ->
return FileManager.getInstance(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")
log.debug("Installed dependencies: $it")
}
} ?: emptyList<String>().also {
logger<DependencyManager>().error("package.json not found")
log.error("package.json not found")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers

import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
Expand All @@ -11,9 +13,13 @@ import com.intellij.psi.search.GlobalSearchScope
import java.io.IOException
import java.nio.file.NoSuchFileException

@Service(Service.Level.PROJECT)
class FileManager(private val project: Project) {
companion object {
private val log = logger<FileManager>()

@JvmStatic
fun getInstance(project: Project): FileManager = project.service()
}

fun saveFileAtPath(file: PsiFile, path: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory

object PsiHelper {

fun createPsiFile(project: Project, fileName: String, text: String): PsiFile {
assert(fileName.contains('.')) { "File name must contain an extension" }
return runReadAction {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers

import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import java.io.File

class ShellRunner(private val project: Project? = null) {
@Service(Service.Level.PROJECT)
class ShellRunner(private val project: Project) {
companion object {
private val log = logger<ShellRunner>()

@JvmStatic
fun getInstance(project: Project): ShellRunner = project.service()
}

private val failedCommands = mutableSetOf<String>()
private val failedWindowsPrograms = mutableSetOf<String>()

private fun isWindows() = System.getProperty("os.name").lowercase().contains("win")

fun execute(command: Array<String>): String? {
val commandName = command.firstOrNull() ?: return null.also {
val program = command.firstOrNull() ?: return null.also {
log.warn("No command name provided")
}
if (isWindows() && failedCommands.contains(commandName)) {
command[0] = "$commandName.cmd"
if (isWindows() && failedWindowsPrograms.contains(program)) {
command[0] = "$program.cmd"
log.warn("(Re)trying command with .cmd extension: \"${command.joinToString(" ")}\"")
}
return try {
val platformCommand = if (isWindows()) {
Expand All @@ -28,21 +35,25 @@ class ShellRunner(private val project: Project? = null) {
} + command
log.debug("Executing command: \"${platformCommand.joinToString(" ")}\"")
val process = ProcessBuilder(*platformCommand)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.directory(project?.basePath?.let { File(it) })

Check warning on line 38 in src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/ShellRunner.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Usage of redundant or deprecated syntax or deprecated symbols

Unnecessary safe call on a non-null receiver of type Project
.start()
process.waitFor()
process.inputStream.bufferedReader().readText().also {
log.debug("Successfully executed \"${platformCommand.joinToString(" ")}\": $it")
val output = process.inputStream?.bufferedReader()?.readText()?.also {
log.debug("Successfully executed \"${platformCommand.joinToString(" ")}\" with output:\n$it")
}
val error = process.errorStream?.bufferedReader()?.readText()
if (output.isNullOrBlank() && !error.isNullOrBlank()) {
log.warn("Error while executing \"${platformCommand.joinToString(" ")}\":\n${error.take(150)}")
}
output
} catch (e: Exception) {
if (isWindows() && !commandName.endsWith(".cmd")) {
if (isWindows() && !program.endsWith(".cmd")) {
log.warn(
"Failed to execute \"${command.joinToString(" ")}\". Trying to execute \"$commandName.cmd\" instead",
"Failed to execute \"${command.joinToString(" ")}\". Trying to execute \"$program.cmd\" instead",
e
)
failedCommands.add(commandName)
return execute(arrayOf("$commandName.cmd") + command.drop(1).toTypedArray())
failedWindowsPrograms.add(program)
return execute(arrayOf("$program.cmd") + command.drop(1).toTypedArray())
}
log.warn("Error while executing \"${command.joinToString(" ")}\"", e)
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
protected open fun getLocalConfig(): C {
return config?.also {
log.debug("Returning cached config")
} ?: FileManager(project).getFileContentsAtPath(configFile)?.let {
} ?: FileManager.getInstance(project).getFileContentsAtPath(configFile)?.let {
log.debug("Parsing config from $configFile")
try {
Json.decodeFromString(serializer, it).also {
Expand Down Expand Up @@ -152,7 +152,7 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}

// Public methods
open fun fetchAllComponents(): List<Component> {
fun fetchAllComponents(): List<Component> {
return RequestSender.sendRequest("$domain/registry/index.json").ok {
Json.decodeFromString<List<Component>>(it.body)
}?.also {
Expand All @@ -162,8 +162,8 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}
}

open fun getInstalledComponents(): List<String> {
return FileManager(project).getFileAtPath(
fun getInstalledComponents(): List<String> {
return FileManager.getInstance(project).getFileAtPath(
"${resolveAlias(getLocalPathForComponents())}/ui"
)?.children?.map { file ->
if (file.isDirectory) file.name else file.name.substringBeforeLast(".")
Expand All @@ -174,12 +174,12 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}
}

open fun addComponent(componentName: String) {
fun addComponent(componentName: String) {
val componentsPath = resolveAlias(getLocalPathForComponents())
// Install component
val component = fetchComponent(componentName)
val installedComponents = getInstalledComponents()
val fileManager = FileManager(project)
val fileManager = FileManager.getInstance(project)
val notifManager = NotificationManager(project)
log.debug("Installing ${component.name} (installed: ${installedComponents.joinToString(", ")})")
setOf(component, *getRegistryDependencies(component).filter {
Expand Down Expand Up @@ -241,7 +241,7 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}

// Install dependencies
val depsManager = DependencyManager(project)
val depsManager = DependencyManager.getInstance(project)
val depsToInstall = component.dependencies.filter { dependency ->
!depsManager.isDependencyInstalled(dependency)
}
Expand Down Expand Up @@ -275,15 +275,15 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}
}

open fun isComponentUpToDate(componentName: String): Boolean {
fun isComponentUpToDate(componentName: String): Boolean {
val remoteComponent = fetchComponent(componentName)
val componentPath =
"${resolveAlias(getLocalPathForComponents())}/${remoteComponent.type.substringAfterLast(":")}${
if (usesDirectoriesForComponents()) {
"/${remoteComponent.name}"
} else ""
}"
val fileManager = FileManager(project)
val fileManager = FileManager.getInstance(project)
return remoteComponent.files.all { file ->
val psiFile = PsiHelper.createPsiFile(
project, adaptFileExtensionToConfig(file.name), file.content
Expand All @@ -302,10 +302,10 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
val componentsDir =
"${resolveAlias(getLocalPathForComponents())}/${remoteComponent.type.substringAfterLast(":")}"
if (usesDirectoriesForComponents()) {
FileManager(project).deleteFileAtPath("$componentsDir/${remoteComponent.name}")
FileManager.getInstance(project).deleteFileAtPath("$componentsDir/${remoteComponent.name}")
} else {
remoteComponent.files.forEach { file ->
FileManager(project).deleteFileAtPath("$componentsDir/${file.name}")
FileManager.getInstance(project).deleteFileAtPath("$componentsDir/${file.name}")
}
}
// Remove dependencies no longer needed by any component
Expand All @@ -314,9 +314,10 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
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
}
val uselessDependencies =
DependencyManager.getInstance(project).getInstalledDependencies().filter { dependency ->
dependency in allPossiblyNeededDependencies && dependency !in currentlyNeededDependencies
}
if (uselessDependencies.isNotEmpty()) {
val multipleDependencies = uselessDependencies.size > 1
val notifManager = NotificationManager(project)
Expand All @@ -331,7 +332,7 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
listOf(
NotificationAction.createSimple("Remove") {
runAsync {
DependencyManager(project).uninstallDependencies(uselessDependencies)
DependencyManager.getInstance(project).uninstallDependencies(uselessDependencies)
}.then {
notifManager.sendNotificationAndHide(
"Removed dependenc${if (multipleDependencies) "ies" else "y"}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ open class ReactSource(project: Project) : Source<ReactConfig>(project, ReactCon
return alias
}
val configFile = if (getLocalConfig().tsx) "tsconfig.json" else "jsconfig.json"
val tsConfig = FileManager(project).getFileContentsAtPath(configFile)
val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(configFile)
?: throw NoSuchFileException("$configFile not found")
val aliasPath = parseTsConfig(tsConfig)
.jsonObject["compilerOptions"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ open class SolidSource(project: Project) : Source<SolidConfig>(project, SolidCon
return alias
}
val configFile = "tsconfig.json"
val tsConfig = FileManager(project).getFileContentsAtPath(configFile)
val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(configFile)
?: throw NoSuchFileException("$configFile not found")
val aliasPath = parseTsConfig(tsConfig)
.jsonObject["compilerOptions"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ open class SolidUISource(project: Project) : Source<SolidUIConfig>(project, Soli
return alias
}
val configFile = if (getLocalConfig().tsx) "tsconfig.json" else "jsconfig.json"
val tsConfig = FileManager(project).getFileContentsAtPath(configFile)
val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(configFile)
?: throw NoSuchFileException("$configFile not found")
val aliasPath = parseTsConfig(tsConfig)
.jsonObject["compilerOptions"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ open class SvelteSource(project: Project) : Source<SvelteConfig>(project, Svelte
log.warn("Alias $alias does not start with $, @ or ~, returning it as-is")
return alias
}
val usesKit = DependencyManager(project).isDependencyInstalled("@sveltejs/kit")
val usesKit = DependencyManager.getInstance(project).isDependencyInstalled("@sveltejs/kit")
val tsConfigName = if (getLocalConfig().typescript) "tsconfig.json" else "jsconfig.json"
val configFile = if (usesKit) ".svelte-kit/$tsConfigName" else tsConfigName
val fileManager = FileManager(project)
val fileManager = FileManager.getInstance(project)
var tsConfig = fileManager.getFileContentsAtPath(configFile)
if (tsConfig == null) {
if (!usesKit) throw NoSuchFileException("Cannot get $configFile")
val res = ShellRunner(project).execute(arrayOf("npx", "svelte-kit", "sync"))
val res = ShellRunner.getInstance(project).execute(arrayOf("npx", "svelte-kit", "sync"))
if (res == null) {
NotificationManager(project).sendNotification(
"Failed to generate $configFile",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ open class VueSource(project: Project) : Source<VueConfig>(project, VueConfig.se
else -> "tsconfig.json"
}.let { if (!config.typescript) "jsconfig.json" else it }

val tsConfig = FileManager(project).getFileContentsAtPath(tsConfigLocation)
val tsConfig = FileManager.getInstance(project).getFileContentsAtPath(tsConfigLocation)
?: throw NoSuchFileException("$tsConfigLocation not found")
val aliasPath = (resolvePath(tsConfig, tsConfigLocation) ?: if (config.typescript) {
resolvePath("tsconfig.app.json", "tsconfig.app.json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ISPPanelPopulator(private val project: Project) {
CoroutineScope(SupervisorJob() + Dispatchers.Default).async {
return@async Pair(
SourceScanner.findShadcnImplementation(project),
FileManager(project).getVirtualFilesByName("package.json").size
FileManager.getInstance(project).getVirtualFilesByName("package.json").size
)
}.asCompletableFuture().thenApplyAsync { (source, packageJsonCount) ->
log.info("Shadcn implementation detected: $source, package.json count: $packageJsonCount")
Expand Down

0 comments on commit 20e620b

Please sign in to comment.