Skip to content

Commit

Permalink
Fix icons and Svelte support, add logs
Browse files Browse the repository at this point in the history
Also log version and fix bug in prod for dotfiles
  • Loading branch information
WarningImHack3r committed Jan 9, 2024
1 parent 06695fa commit ec3d4cf
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.Reac
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.SolidSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.SvelteSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.VueSource
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project

object SourceScanner {

fun findShadcnImplementation(project: Project): Source<*>? {
return FileManager(project).getVirtualFilesByName("components.json").firstOrNull()?.let { componentsJson ->
return FileManager(project).getFileAtPath("components.json")?.let { componentsJson ->
val contents = componentsJson.contentsToByteArray().decodeToString()
when {
contents.contains("shadcn-svelte.com") -> SvelteSource(project)
Expand All @@ -21,6 +22,10 @@ object SourceScanner {
contents.contains("shadcn-solid") -> SolidSource(project)
else -> null
}
}.also {
val log = logger<SourceScanner>()
if (it == null) log.warn("No shadcn implementation found")
else log.info("Found shadcn implementation: ${it.javaClass.name}")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers

import com.github.warningimhack3r.intellijshadcnplugin.notifications.NotificationManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
Expand All @@ -23,12 +26,21 @@ class DependencyManager(private val project: Project) {
fileManager.getVirtualFilesByName(it.key).isNotEmpty()
} }.values.firstOrNull()
// install the dependency
ShellRunner(project).execute(listOfNotNull(
val command = listOfNotNull(
packageManager,
"i",
if (installationType == InstallationType.DEV) "-D" else null,
dependencyName
).toTypedArray())
).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
)
}
}

fun installDependencies(dependencyNames: List<String>, installationType: InstallationType = InstallationType.PROD) {
Expand All @@ -37,11 +49,13 @@ class DependencyManager(private val project: Project) {

fun isDependencyInstalled(dependency: String): Boolean {
// Read the package.json file
return FileManager(project).getVirtualFilesByName("package.json").firstOrNull()?.let { packageJson ->
return FileManager(project).getFileAtPath("package.json")?.let { packageJson ->
val contents = packageJson.contentsToByteArray().decodeToString()
Json.parseToJsonElement(contents).jsonObject.filter {
it.key == "dependencies" || it.key == "devDependencies"
}.map { it.value.jsonObject.keys }.flatten().contains(dependency)
}.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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers

import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
Expand All @@ -9,14 +10,18 @@ import java.io.IOException
import java.nio.file.NoSuchFileException

class FileManager(private val project: Project) {
private val log = logger<FileManager>()

fun saveFileAtPath(file: PsiFile, path: String) {
var deepest = getDeepestFileForPath(path)
val deepestRelativePath = deepest.path.substringAfter("${project.basePath!!}/")
path.substringAfter(deepestRelativePath).split('/').filterNot { it.isEmpty() }.forEach { subdirectory ->
path.substringAfter(deepestRelativePath).split('/').filterNot { it.isEmpty() }.also {
log.debug("Creating subdirectories ${it.joinToString(", ")}")
}.forEach { subdirectory ->
deepest = deepest.createChildDirectory(this, subdirectory)
}
deepest.createChildData(this, file.name).apply {
setBinaryContent(file.text.toByteArray())
deepest.createChildData(this, file.name).setBinaryContent(file.text.toByteArray()).also {
log.debug("Saved file ${file.name} under ${deepest.path}")
}
}

Expand All @@ -25,44 +30,71 @@ class FileManager(private val project: Project) {
getFileAtPath(path)?.delete(this)?.let { true } ?: false
} catch (e: IOException) {
false
}.also {
if (!it) log.warn("Unable to delete file at path $path")
else log.debug("Deleted file at path $path")
}
}

fun getVirtualFilesByName(name: String): Collection<VirtualFile> {
return FilenameIndex.getVirtualFilesByName(
name,
GlobalSearchScope.projectScope(project)
).filter { file ->
val nodeModule = file.path.contains("node_modules")
if (!name.startsWith(".")) {
!nodeModule && !file.path.substringAfter(project.basePath!!).startsWith(".")
} else !nodeModule
}.sortedBy { file ->
return (if (name.startsWith('.')) {
log.debug("Using workaround to find files named $name")
// For some reason, dotfiles/folders don't show up with
// a simple call to FilenameIndex.getVirtualFilesByName.
// This is a dirty workaround to make it work on production,
// because it works fine during local development.
FilenameIndex.getVirtualFilesByName(
"components.json",
GlobalSearchScope.projectScope(project)
).firstOrNull().also {
if (it == null) {
log.warn("components.json not found with the workaround")
}
}?.parent?.children?.filter {
it.name.contains(name)
} ?: listOf<VirtualFile?>().also {
log.warn("No file named $name found with the workaround")
}
} else {
FilenameIndex.getVirtualFilesByName(
name,
GlobalSearchScope.projectScope(project)
)
}).sortedBy { file ->
name.toRegex().find(file.path)?.range?.first ?: Int.MAX_VALUE
}.also {
log.debug("Found ${it.size} files named $name: ${it.toList()}")
}
}

private fun getDeepestFileForPath(filePath: String): VirtualFile {
var paths = filePath.split('/')
var currentFile = getVirtualFilesByName(paths.first()).firstOrNull() ?: throw NoSuchFileException("No file found at path $filePath")
var currentFile = getVirtualFilesByName(paths.first()).firstOrNull() ?: throw NoSuchFileException("No file found at path $filePath").also {
log.warn("No file found at path ${paths.first()}")
}
paths = paths.drop(1)
for (path in paths) {
val child = currentFile.findChild(path)
if (child == null) {
return currentFile
break
} else {
currentFile = child
}
}
return currentFile
return currentFile.also {
log.debug("Found deepest file for path $filePath: ${it.path}")
}
}

fun getFileAtPath(filePath: String): VirtualFile? {
try {
return try {
val deepest = getDeepestFileForPath(filePath)
return if (deepest.name == filePath.substringAfterLast('/')) deepest else null
if (deepest.name == filePath.substringAfterLast('/')) deepest else null
} catch (e: Exception) {
return null
null
}.also {
if (it == null) log.warn("No file found at path $filePath")
else log.debug("Found file at path $filePath: ${it.path}")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,42 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers

import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.jetbrains.rd.util.printlnError
import java.io.File

class ShellRunner(private val project: Project? = null) {
private val log = logger<ShellRunner>()
private val failedCommands = mutableSetOf<String>()

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

fun execute(command: Array<String>): String? {
val commandName = command.firstOrNull() ?: return null
val commandName = command.firstOrNull() ?: return null.also {
log.error("No command name provided")
}
if (isWindows() && failedCommands.contains(commandName)) {
command[0] = "$commandName.cmd"
}
return try {
val process = ProcessBuilder(*command)
val platformCommand = if (isWindows()) {
arrayOf("cmd", "/c")
} else {
arrayOf("sh", "-c")
} + command
val process = ProcessBuilder(*platformCommand)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.directory(project?.basePath?.let { File(it) })
.start()
process.waitFor()
process.inputStream.bufferedReader().readText()
process.inputStream.bufferedReader().readText().also {
log.debug("Successfully executed \"${platformCommand.joinToString(" ")}\": $it")
}
} catch (e: Exception) {
if (isWindows() && !commandName.endsWith(".cmd")) {
failedCommands.add(commandName)
return execute(arrayOf("$commandName.cmd") + command.drop(1).toTypedArray())
}
printlnError("Error while executing \"${command.joinToString(" ")}\": ${e.message}")
log.error("Error while executing \"${command.joinToString(" ")}\"", e)
null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.http

import com.intellij.openapi.diagnostic.logger
import java.net.HttpURLConnection
import java.net.URL

// Credit to: https://gist.github.com/GrzegorzDyrda/be47602fc855a52fba240dd2c2adc2d5
object RequestSender {
private val log = logger<RequestSender>()

/**
* Sends an HTTP request to the given [url], using the given HTTP [method]. The request can also
Expand All @@ -28,6 +30,7 @@ object RequestSender {
}

if (conn.responseCode in 300..399) {
log.debug("Redirecting from ${conn.url} to ${conn.getHeaderField("Location")}")
return sendRequest(conn.getHeaderField("Location"), method, mapOf(
"Cookie" to conn.getHeaderField("Set-Cookie")
).filter { it.value != null }, body)
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.diagnostic.logger
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFileFactory
Expand All @@ -20,19 +21,26 @@ import java.net.URI
import java.nio.file.NoSuchFileException

abstract class Source<C : Config>(val project: Project, private val serializer: KSerializer<C>) {
private val log = logger<Source<*>>()
abstract var framework: String
private val domain: String
get() = URI(getLocalConfig().`$schema`).let { uri ->
"${uri.scheme}://${uri.host}"
"${uri.scheme}://${uri.host}".also {
log.debug("Parsed domain: $it")
}
}

// Utility methods
protected fun getLocalConfig(): C {
val file = "components.json"
return FileManager(project).getFileContentsAtPath(file)?.let {
log.debug("Parsing config from $file")
try {
Json.decodeFromString(serializer, it)
Json.decodeFromString(serializer, it).also { config ->
log.debug("Parsed config: ${config.javaClass.name}")
}
} catch (e: Exception) {
log.error("Unable to parse $file", e)
throw UnparseableConfigException(project, "Unable to parse $file", e)
}
} ?: throw NoSuchFileException("$file not found")
Expand All @@ -53,14 +61,18 @@ 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")
return response.ok { Json.decodeFromString(it.body) } ?: throw Exception("Component not found").also {
log.error("Unable to fetch component $componentName", it)
}
}

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")
} ?: throw Exception("Colors not found").also {
log.error("Unable to fetch colors", it)
}
}

protected open fun getRegistryDependencies(component: ComponentWithContents): List<ComponentWithContents> {
Expand All @@ -74,24 +86,35 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
open fun fetchAllComponents(): List<ISPComponent> {
return RequestSender.sendRequest("$domain/registry/index.json").ok {
Json.decodeFromString<List<Component>>(it.body)
}?.map { ISPComponent(it.name) } ?: emptyList()
}?.map { ISPComponent(it.name) }?.also {
log.info("Fetched ${it.size} remote components: ${it.joinToString(", ") { component -> component.name }}")
} ?: emptyList<ISPComponent>().also {
log.error("Unable to fetch remote components")
}
}

open fun getInstalledComponents(): List<String> {
return FileManager(project).getFileAtPath(
"${resolveAlias(getLocalConfig().aliases.components)}/ui"
)?.children?.map { file ->
if (file.isDirectory) file.name else file.name.substringBeforeLast(".")
}?.sorted() ?: emptyList()
}?.sorted()?.also {
log.info("Fetched ${it.size} installed components: ${it.joinToString(", ")}")
} ?: emptyList<String>().also {
log.error("Unable to fetch installed components")
}
}

open fun addComponent(componentName: String) {
// Install component
val component = fetchComponent(componentName)
val installedComponents = getInstalledComponents()
log.debug("Installing ${component.name} (installed: ${installedComponents.joinToString(", ")})")
setOf(component, *getRegistryDependencies(component).filter {
!installedComponents.contains(it.name)
}.toTypedArray<ComponentWithContents>()).forEach { downloadedComponent ->
}.toTypedArray<ComponentWithContents>()).also {
log.debug("Installing ${it.size} components: ${it.joinToString(", ") { component -> component.name }}")
}.forEach { downloadedComponent ->
downloadedComponent.files.forEach { file ->
val psiFile = PsiFileFactory.getInstance(project).createFileFromText(
adaptFileExtensionToConfig(file.name),
Expand All @@ -113,6 +136,7 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
!depsManager.isDependencyInstalled(dependency)
}
if (depsToInstall.isEmpty()) return
log.debug("Installing ${depsToInstall.size} dependencies: ${depsToInstall.joinToString(", ") { dependency -> dependency }}")
val dependenciesList = with(depsToInstall) {
if (size == 1) first() else {
"${dropLast(1).joinToString(", ")} and ${last()}"
Expand Down Expand Up @@ -145,11 +169,13 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
open fun isComponentUpToDate(componentName: String): Boolean {
val remoteComponent = fetchComponent(componentName)
return remoteComponent.files.all { file ->
FileManager(project).getFileContentsAtPath(
(FileManager(project).getFileContentsAtPath(
"${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${if (usesDirectoriesForComponents()) {
"/${remoteComponent.name}"
} else ""}/${file.name}"
) == adaptFileToConfig(file.content)
) == adaptFileToConfig(file.content)).also {
log.debug("File ${file.name} for ${remoteComponent.name} is ${if (it) "" else "NOT "}up to date")
}
}
}

Expand Down
Loading

0 comments on commit ec3d4cf

Please sign in to comment.