Skip to content

Commit

Permalink
Complete refactor
Browse files Browse the repository at this point in the history
Use inheritance, generics & factorization to completely refactor the framework support workflow.
Additionally rename files, removing useless prefixes and fix bugs.
  • Loading branch information
WarningImHack3r committed Jan 8, 2024
1 parent f008ec5 commit f434e51
Show file tree
Hide file tree
Showing 24 changed files with 987 additions and 1,570 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend

import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.FileManager
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.ISPSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.ISPReactSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.ISPSolidSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.ISPSvelteSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.ISPVueSource
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl.ReactSource
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.project.Project

object ISPScanner {
object SourceScanner {

fun findShadcnImplementation(project: Project): ISPSource? {
fun findShadcnImplementation(project: Project): Source<*>? {
return FileManager(project).getVirtualFilesByName("components.json").firstOrNull()?.let { componentsJson ->
val contents = componentsJson.contentsToByteArray().decodeToString()
when {
contents.contains("shadcn-svelte.com") -> ISPSvelteSource(project)
contents.contains("ui.shadcn.com") -> ISPReactSource(project)
contents.contains("shadcn-vue.com") -> ISPVueSource(project)
contents.contains("shadcn-solid") -> ISPSolidSource(project)
contents.contains("shadcn-svelte.com") -> SvelteSource(project)
contents.contains("ui.shadcn.com") -> ReactSource(project)
contents.contains("shadcn-vue.com") -> VueSource(project)
contents.contains("shadcn-solid") -> SolidSource(project)
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,29 @@ object RequestSender {
requestMethod = method
doOutput = body != null
headers?.forEach(::setRequestProperty)
}

if (body != null) {
conn.outputStream.use {
it.write(body.toByteArray())
body?.let {
outputStream.use {
it.write(body.toByteArray())
}
}
}

val responseBody = conn.inputStream.use { it.readBytes() }.toString(Charsets.UTF_8)
if (conn.responseCode in 300..399) {
return sendRequest(conn.getHeaderField("Location"), method, mapOf(
"Cookie" to conn.getHeaderField("Set-Cookie")
).filter { it.value != null }, body)
}

return Response(conn.responseCode, conn.headerFields, responseBody)
return Response(conn.responseCode, conn.headerFields, conn.inputStream.bufferedReader().readText())
}

data class Response(val statusCode: Int, val headers: Map<String, List<String>>? = null, val body: String? = null)
data class Response(val statusCode: Int, val headers: Map<String, List<String>>, val body: String) {

fun <T> ok(action: (Response) -> T): T? {
if (statusCode in 200..299) {
return action(this)
}
return null
}
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.sources

import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.DependencyManager
import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.FileManager
import com.github.warningimhack3r.intellijshadcnplugin.backend.http.RequestSender
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config.Config
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.Component
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.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFileFactory
import kotlinx.serialization.KSerializer
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import org.jetbrains.concurrency.runAsync
import java.net.URI
import java.nio.file.NoSuchFileException

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

// Utility methods
protected fun getLocalConfig(): C {
return FileManager(project).getFileContentsAtPath("components.json")?.let {
Json.decodeFromString(serializer, it)
} ?: throw NoSuchFileException("components.json not found")
}

protected abstract fun resolveAlias(alias: String): String

protected fun cleanAlias(alias: String): String = if (alias.startsWith("\$")) {
"\\$alias" // fixes Kotlin silently crashing when the replacement starts with $ with a regex
} else alias

protected open fun adaptFileExtensionToConfig(extension: String): String = extension

protected abstract fun adaptFileToConfig(contents: String): String

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")
}

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

// Public methods
open fun fetchAllComponents(): List<ISPComponent> {
val response = RequestSender.sendRequest("$domain/registry/index.json")
return response.ok {
Json.decodeFromString<List<Component>>(it.body)
}?.map { ISPComponent(it.name) } ?: emptyList()
}

open fun getInstalledComponents(): List<String> {
return FileManager(project).getFileAtPath(
"${resolveAlias(getLocalConfig().aliases.components)}/ui"
)?.children?.map { it.name }?.sorted() ?: emptyList()
}

open fun addComponent(componentName: String) {
val installedComponents = getInstalledComponents()
fun getRegistryDependencies(component: ComponentWithContents): List<ComponentWithContents> {
return component.registryDependencies.filter {
!installedComponents.contains(it)
}.map { registryDependency ->
val dependency = fetchComponent(registryDependency)
listOf(dependency, *getRegistryDependencies(dependency).toTypedArray())
}.flatten()
}

// Install component
val component = fetchComponent(componentName)
val components = setOf(component, *getRegistryDependencies(fetchComponent(componentName)).toTypedArray())
val config = getLocalConfig()
components.forEach { downloadedComponent ->
downloadedComponent.files.forEach { file ->
val path = "${resolveAlias(config.aliases.components)}/${component.type.substringAfterLast(":")}/${downloadedComponent.name}"
val psiFile = PsiFileFactory.getInstance(project).createFileFromText(
adaptFileExtensionToConfig(file.name),
FileTypeManager.getInstance().getFileTypeByExtension(
adaptFileExtensionToConfig(file.name).substringAfterLast('.')
),
adaptFileToConfig(file.content)
)
FileManager(project).saveFileAtPath(psiFile, path)
}
}

// Install dependencies
val depsManager = DependencyManager(project)
val depsToInstall = component.dependencies.filter { dependency ->
!depsManager.isDependencyInstalled(dependency)
}
if (depsToInstall.isEmpty()) return
val dependenciesList = with(depsToInstall) {
if (size == 1) first() else {
"${dropLast(1).joinToString(", ")} and ${last()}"
}
}
val notifManager = NotificationManager(project)
notifManager.sendNotification(
"Installed ${component.name}",
"${component.name} requires $dependenciesList to be installed."
) { notif ->
mapOf(
"Install" to DependencyManager.InstallationType.PROD,
"Install as dev" to DependencyManager.InstallationType.DEV
).map { (label, installType) ->
NotificationAction.createSimple(label) {
runAsync {
depsManager.installDependencies(depsToInstall, installType)
}.then {
notifManager.sendNotificationAndHide(
"Installed $dependenciesList",
"Installed $dependenciesList for ${component.name}.",
)
}
notif.hideBalloon()
}
}
}
}

open fun isComponentUpToDate(componentName: String): Boolean {
val config = getLocalConfig()
val remoteComponent = fetchComponent(componentName)
return remoteComponent.files.all { file ->
FileManager(project).getFileContentsAtPath(
"${resolveAlias(config.aliases.components)}/${remoteComponent.type.substringAfterLast(":")}/${remoteComponent.name}/${file.name}"
) == adaptFileToConfig(file.content)
}
}

open fun removeComponent(componentName: String) {
val remoteComponent = fetchComponent(componentName)
FileManager(project).deleteFileAtPath(
"${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}/${remoteComponent.name}"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config

import kotlinx.serialization.Serializable

/**
* A shadcn-svelte locally installed components.json file.
*/
@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
sealed class Config {
/**
* The schema URL for the file.
*/
abstract val `$schema`: String

Check notice on line 14 in src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/Config.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Property naming convention

Property name ```$schema``` should start with a lowercase letter

Check warning on line 14 in src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/Config.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Property "$schema" is never used
/**
* The library's style used.
*/
abstract val style: String
/**
* The Tailwind configuration.
*/
abstract val tailwind: Tailwind
/**
* The aliases for the components and utils directories.
*/
abstract val aliases: Aliases

/**
* The Tailwind configuration.
*/
@Serializable
sealed class Tailwind {
/**
* The relative path to the Tailwind config file.
*/
abstract val config: String
/**
* The relative path of the Tailwind CSS file.
*/
abstract val css: String
/**
* The library's base color.
*/
abstract val baseColor: String
}

/**
* The aliases for the components and utils directories.
*/
@Serializable
sealed class Aliases {
/**
* The alias for the components' directory.
*/
abstract val components: String
/**
* The alias for the utils directory.
*/
abstract val utils: String
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config

import kotlinx.serialization.Serializable

/**
* A shadcn-svelte locally installed components.json file.
* @param `$schema` The schema URL for the file.
* @param style The library style used.
* @param tailwind The Tailwind configuration.
* @param rsc Whether to support React Server Components.
* @param tsx Whether to use TypeScript over JavaScript.
* @param aliases The aliases for the components and utils directories.
*/
@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
class ReactConfig(
override val `$schema`: String,
override val style: String,
override val tailwind: Tailwind,
val rsc: Boolean,
val tsx: Boolean = true,
override val aliases: Aliases
) : Config() {

/**
* The Tailwind configuration.
* @param config The relative path to the Tailwind config file.
* @param css The relative path of the Tailwind CSS file.
* @param baseColor The library's base color.
* @param cssVariables Whether to use CSS variables or utility classes.
* @param prefix The prefix to use for utility classes.
*/
@Serializable
class Tailwind(
override val config: String,
override val css: String,
override val baseColor: String,
val cssVariables: Boolean,
val prefix: String = ""
) : Config.Tailwind()

/**
* The aliases for the components and utils directories.
* @param components The alias for the components' directory.
* @param utils The alias for the utils directory.
* @param ui The alias for UI components.
*/
@Serializable
class Aliases(
override val components: String,
override val utils: String,
val ui: String? = null
) : Config.Aliases()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config

import kotlinx.serialization.Serializable

@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
class SolidConfig(
override val `$schema`: String,
override val style: String,
override val tailwind: VueConfig.Tailwind,
override val aliases: Aliases
) : Config() {

@Serializable
class Aliases(
override val components: String,
override val utils: String
) : Config.Aliases()
}
Loading

0 comments on commit f434e51

Please sign in to comment.