Skip to content

Commit

Permalink
Fix support for all frameworks
Browse files Browse the repository at this point in the history
- Fix incorrect support for frameworks that don't use folders for components
- Fix missing default value for some frameworks
- Globally fix alias resolution
- Fix imports replacement being broken for some frameworks
- Fix support for Vue as its config doesn't contain schema by default for some reason
  • Loading branch information
WarningImHack3r committed Jan 8, 2024
1 parent c9c2be8 commit 2a336f1
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 84 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
- Rework `class`es replacement detection mechanism to be 100% accurate
- Add tests for this
- Add support for Vue's `typescript` option (transpiling TypeScript to JavaScript in `*.vue` files)
- Parse `vite.config.(js|ts)` to resolve aliases as a fallback of `tsconfig.json`

## Description

Expand Down Expand Up @@ -38,7 +37,9 @@ If you don't see the tool window, you can open it from `View > Tool Windows > sh

## Planned Features

- Parse `vite.config.(js|ts)` (and others like `nuxt.config.ts`) to resolve aliases as a fallback of `tsconfig.json`
- Figure out a clean way to refresh the tool window automatically after adding or removing components
- Refresh/recreate the tool window automatically when the project finishes indexing
- Add support for monorepos
<!-- Plugin description end -->

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ object SourceScanner {
when {
contents.contains("shadcn-svelte.com") -> SvelteSource(project)
contents.contains("ui.shadcn.com") -> ReactSource(project)
contents.contains("shadcn-vue.com") -> VueSource(project)
contents.contains("shadcn-vue.com")
|| contents.contains("\"framework\": \"") -> VueSource(project)
contents.contains("shadcn-solid") -> SolidSource(project)
else -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import java.io.IOException
import java.nio.file.NoSuchFileException

class FileManager(private val project: Project) {
Expand All @@ -20,7 +21,11 @@ class FileManager(private val project: Project) {
}

fun deleteFileAtPath(path: String): Boolean {
return getFileAtPath(path)?.delete(this)?.let { true } ?: false
return try {
getFileAtPath(path)?.delete(this)?.let { true } ?: false
} catch (e: IOException) {
false
}
}

fun getVirtualFilesByName(name: String): Collection<VirtualFile> {
Expand All @@ -32,6 +37,8 @@ class FileManager(private val project: Project) {
if (!name.startsWith(".")) {
!nodeModule && !file.path.substringAfter(project.basePath!!).startsWith(".")
} else !nodeModule
}.sortedBy { file ->
name.toRegex().find(file.path)?.range?.first ?: Int.MAX_VALUE
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ abstract class Source<C : Config>(val project: Project, private val serializer:

// Utility methods
protected fun getLocalConfig(): C {
return FileManager(project).getFileContentsAtPath("components.json")?.let {
val file = "components.json"
return FileManager(project).getFileContentsAtPath(file)?.let {
try {
Json.decodeFromString(serializer, it)
} catch (e: Exception) {
throw UnparseableConfigException(project, "Unable to parse components.json", e)
throw UnparseableConfigException(project, "Unable to parse $file", e)
}
} ?: throw NoSuchFileException("components.json not found")
} ?: throw NoSuchFileException("$file not found")
}

protected abstract fun usesDirectoriesForComponents(): Boolean

protected abstract fun resolveAlias(alias: String): String

protected fun cleanAlias(alias: String): String = if (alias.startsWith("\$")) {
Expand All @@ -55,49 +58,51 @@ abstract class Source<C : Config>(val project: Project, private val serializer:

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")
return RequestSender.sendRequest("$domain/registry/colors/$baseColor.json").ok {
Json.parseToJsonElement(it.body)
} ?: throw Exception("Colors not found")
}

protected open fun getRegistryDependencies(component: ComponentWithContents): List<ComponentWithContents> {
return component.registryDependencies.map { registryDependency ->
val dependency = fetchComponent(registryDependency)
listOf(dependency, *getRegistryDependencies(dependency).toTypedArray())
}.flatten()
}

// Public methods
open fun fetchAllComponents(): List<ISPComponent> {
val response = RequestSender.sendRequest("$domain/registry/index.json")
return response.ok {
return RequestSender.sendRequest("$domain/registry/index.json").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()
)?.children?.map { file ->
if (file.isDirectory) file.name else file.name.substringBeforeLast(".")
}?.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 ->
val installedComponents = getInstalledComponents()
setOf(component, *getRegistryDependencies(component).filter {
!installedComponents.contains(it.name)
}.toTypedArray<ComponentWithContents>()).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)
)
val path = "${resolveAlias(getLocalConfig().aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) {
"/${downloadedComponent.name}"
} else ""
FileManager(project).saveFileAtPath(psiFile, path)
}
}
Expand Down Expand Up @@ -138,19 +143,25 @@ abstract class Source<C : Config>(val project: Project, private val serializer:
}

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}"
"${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${if (usesDirectoriesForComponents()) {
"/${remoteComponent.name}"
} else ""}/${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}"
)
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}")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlinx.serialization.Serializable
@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
class VueConfig(
override val `$schema`: String,
override val `$schema`: String = "https://shadcn-vue.com/schema.json",
override val style: String,
val typescript: Boolean = true,
override val tailwind: Tailwind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import java.nio.file.NoSuchFileException
class ReactSource(project: Project) : Source<ReactConfig>(project, ReactConfig.serializer()) {
override var framework = "React"

override fun usesDirectoriesForComponents() = false

override fun resolveAlias(alias: String): String {
if (!alias.startsWith("$") && !alias.startsWith("@")) return alias
val tsConfig = FileManager(project).getFileContentsAtPath("tsconfig.json") ?: throw NoSuchFileException("tsconfig.json not found")
val configFile = if (getLocalConfig().tsx) "tsconfig.json" else "jsconfig.json"
val tsConfig = FileManager(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found")
val aliasPath = Json.parseToJsonElement(tsConfig)
.jsonObject["compilerOptions"]
?.jsonObject?.get("paths")
Expand All @@ -37,23 +40,24 @@ class ReactSource(project: Project) : Source<ReactConfig>(project, ReactConfig.s

override fun adaptFileToConfig(contents: String): String {
val config = getLocalConfig()
// Note: this condition does not replace UI paths (= $components/$ui) by the components path
// if the UI alias is not set.
// For me, this is a bug, but I'm following what the original code does for parity
// (https://github.com/shadcn-ui/ui/blob/fb614ac2921a84b916c56e9091aa0ae8e129c565/packages/cli/src/utils/transformers/transform-import.ts#L10-L23).
var newContents = if (config.aliases.ui != null) {
contents.replace(
Regex("@/registry/[^/]+/ui"), cleanAlias(config.aliases.ui)
// Note: this does not prevent additional imports other than "cn" from being replaced,
// but I'm once again following what the original code does for parity
// (https://github.com/shadcn-ui/ui/blob/fb614ac2921a84b916c56e9091aa0ae8e129c565/packages/cli/src/utils/transformers/transform-import.ts#L25-L35).
val newContents = Regex(".*\\{.*[ ,\n\t]+cn[ ,].*}.*\"(@/lib/cn).*").replace(
// Note: this condition does not replace UI paths (= $components/$ui) by the components path
// if the UI alias is not set.
// For me, this is a bug, but I'm following what the original code does for parity
// (https://github.com/shadcn-ui/ui/blob/fb614ac2921a84b916c56e9091aa0ae8e129c565/packages/cli/src/utils/transformers/transform-import.ts#L10-L23).
if (config.aliases.ui != null) {
contents.replace(
Regex("@/registry/[^/]+/ui"), cleanAlias(config.aliases.ui)
)
} else contents.replace(
Regex("@/registry/[^/]+"), cleanAlias(config.aliases.components)
)
} else contents.replace(
Regex("@/registry/[^/]+"), cleanAlias(config.aliases.components)
)
newContents = newContents.replace(
// Note: this does not prevent additional imports other than "cn" from being replaced,
// but I'm once again following what the original code does for parity
// (https://github.com/shadcn-ui/ui/blob/fb614ac2921a84b916c56e9091aa0ae8e129c565/packages/cli/src/utils/transformers/transform-import.ts#L25-L35).
Regex(".*\\{.*[ ,\n\t]+cn[ ,].*}.*\"@/lib/utils"), config.aliases.utils
).applyIf(config.rsc) {
) { result ->
result.groupValues[0].replace(result.groupValues[1], config.aliases.utils)
}.applyIf(config.rsc) {
replace(
Regex("\"use client\";*\n"), ""
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,32 @@ import java.nio.file.NoSuchFileException
class SolidSource(project: Project) : Source<SolidConfig>(project, SolidConfig.serializer()) {
override var framework = "Solid"

override fun usesDirectoriesForComponents() = false

override fun resolveAlias(alias: String): String {
if (!alias.startsWith("$") && !alias.startsWith("@")) return alias
val tsConfig = FileManager(project).getFileContentsAtPath("tsconfig.json") ?: throw NoSuchFileException("tsconfig.json not found")
val configFile = "tsconfig.json"
val tsConfig = FileManager(project).getFileContentsAtPath(configFile) ?: throw NoSuchFileException("$configFile not found")
val aliasPath = Json.parseToJsonElement(tsConfig)
.jsonObject["compilerOptions"]
?.jsonObject?.get("paths")
?.jsonObject?.get("${alias.substringBefore("/")}/*")
?.jsonArray?.get(0)
?.jsonPrimitive?.content ?: throw Exception("Cannot find alias $alias")
return aliasPath.replace(Regex("^\\./"), "")
return aliasPath.replace(Regex("^\\.+/"), "")
.replace(Regex("\\*$"), alias.substringAfter("/"))
}

override fun adaptFileToConfig(contents: String): String {
val config = getLocalConfig()
val newContents = contents.replace(
Regex("@/registry/[^/]+"), cleanAlias(config.aliases.components)
).replace(
// Note: this does not prevent additional imports other than "cn" from being replaced,
// but I'm following what the original code does for parity
// (https://github.com/hngngn/shadcn-solid/blob/b808e0ecc9fd4689572d9fc0dfb7af81606a11f2/packages/cli/src/utils/transformers/transform-import.ts#L20-L29).
Regex(".*\\{.*[ ,\n\t]+cn[ ,].*}.*\"@/lib/cn"), config.aliases.utils
)
// Note: this does not prevent additional imports other than "cn" from being replaced,
// but I'm following what the original code does for parity
// (https://github.com/hngngn/shadcn-solid/blob/b808e0ecc9fd4689572d9fc0dfb7af81606a11f2/packages/cli/src/utils/transformers/transform-import.ts#L20-L29).
val newContents = Regex(".*\\{.*[ ,\n\t]+cn[ ,].*}.*\"(@/lib/cn).*").replace(
contents.replace(
Regex("@/registry/[^/]+"), cleanAlias(config.aliases.components)
)
) { it.groupValues[0].replace(it.groupValues[1], config.aliases.utils) }

return if (!config.tailwind.cssVariables) {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import java.nio.file.NoSuchFileException
class SvelteSource(project: Project) : Source<SvelteConfig>(project, SvelteConfig.serializer()) {
override var framework = "Svelte"

override fun usesDirectoriesForComponents() = true

override fun resolveAlias(alias: String): String {
if (!alias.startsWith("$") && !alias.startsWith("@")) return alias
var tsConfig = FileManager(project).getFileContentsAtPath(".svelte-kit/tsconfig.json")
Expand Down
Loading

0 comments on commit 2a336f1

Please sign in to comment.