diff --git a/CHANGELOG.md b/CHANGELOG.md
index 482ff95..2b615e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,13 @@
## [Unreleased]
+### Changed
+
+- Entirely rewrite the Tailwind classes replacement engine to be more accurate and faster
+ - Consequently, the plugin now depends
+ on [Svelte](https://plugins.jetbrains.com/plugin/12375-svelte) & [Vue](https://plugins.jetbrains.com/plugin/9442-vue-js)
+ extensions and only works on WebStorm or IntelliJ IDEA Ultimate
+
## [0.7.7] - 2024-03-29
### Changed
diff --git a/README.md b/README.md
index 5fda2d6..d338a9c 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,20 @@
[![Version](https://img.shields.io/jetbrains/plugin/v/com.github.warningimhack3r.intellijshadcnplugin.svg)](https://plugins.jetbrains.com/plugin/com.github.warningimhack3r.intellijshadcnplugin)
[![Downloads](https://img.shields.io/jetbrains/plugin/d/com.github.warningimhack3r.intellijshadcnplugin.svg)](https://plugins.jetbrains.com/plugin/com.github.warningimhack3r.intellijshadcnplugin)
-## ToDo list before 1.0.0
+## 1.0.0 roadmap
-- Rework `class`es replacement detection mechanism to be 100% accurate
- - Add tests for this
- Add support for Vue `typescript` option (transpiling TypeScript to JavaScript as well as in `*.vue` files)
+ - See https://github.com/radix-vue/shadcn-vue/issues/378
## Description
Manage your shadcn/ui components in your project. Supports Svelte, React, Vue, and Solid.
-This plugin will help you manage your shadcn/ui components through a simple tool window. Add, remove, update them with a single click.
-**This plugin will only work with an existing `components.json` file. Manually copied components will not be detected otherwise.**
+This plugin will help you manage your shadcn/ui components through a simple tool window. Add, remove, update them with a
+single click.
+**This plugin will only work with an existing `components.json` file. Manually copied components will not be detected
+otherwise.**
## Features
@@ -33,7 +34,8 @@ This plugin will help you manage your shadcn/ui components through a simple tool
Simply open the `shadcn/ui` tool window and start managing your components.
If you don't see the tool window, you can open it from `View > Tool Windows > shadcn/ui`.
-**When adding or removing components, the tool window won't refresh automatically yet. You can refresh it by closing and reopening it.**
+**When adding or removing components, the tool window won't refresh automatically yet. You can refresh it by closing and
+reopening it.**
## Planned Features
@@ -44,21 +46,23 @@ If you don't see the tool window, you can open it from `View > Tool Windows > sh
- 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
+
## Installation
- Using the IDE built-in plugin system:
-
- Settings/Preferences > Plugins > Marketplace > Search for "intellij-shadcn-plugin" >
+
+ Settings/Preferences > Plugins > Marketplace > Search for "
+ intellij-shadcn-plugin" >
Install
-
+
- Manually:
- Download the [latest release](https://github.com/WarningImHack3r/intellij-shadcn-plugin/releases/latest) and install it manually using
+ Download the [latest release](https://github.com/WarningImHack3r/intellij-shadcn-plugin/releases/latest) and install
+ it manually using
Settings/Preferences > Plugins > ⚙️ > Install plugin from disk...
-
---
Plugin based on the [IntelliJ Platform Plugin Template][template].
diff --git a/gradle.properties b/gradle.properties
index 997550d..a3c815a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,19 +4,19 @@ 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.7
+pluginVersion = 0.8.0
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 213
pluginUntilBuild =
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
-platformType = IC
+platformType = IU
platformVersion = 2021.3
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
-platformPlugins =
+platformPlugins = JavaScript, dev.blachut.svelte.lang:0.21.1, org.jetbrains.plugins.vue:213.5744.223
# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.6
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt
index 055e964..9492b1e 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/DependencyManager.kt
@@ -2,7 +2,6 @@ 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
@@ -22,9 +21,7 @@ class DependencyManager(private val project: Project) {
"yarn.lock" to "yarn",
"bun.lockb" to "bun"
).filter {
- runReadAction {
- fileManager.getVirtualFilesByName(it.key).isNotEmpty()
- }
+ fileManager.getVirtualFilesByName(it.key).isNotEmpty()
}.values.firstOrNull()
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt
index f51d649..f458657 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/FileManager.kt
@@ -1,5 +1,7 @@
package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
@@ -20,16 +22,18 @@ class FileManager(private val project: Project) {
path.substringAfter(deepestRelativePath).split('/').filterNot { it.isEmpty() }.also {
log.debug("Creating subdirectories ${it.joinToString(", ")}")
}.forEach { subdirectory ->
- deepest = deepest.createChildDirectory(this, subdirectory)
+ deepest = runWriteAction { deepest.createChildDirectory(this, subdirectory) }
}
- deepest.createChildData(this, file.name).setBinaryContent(file.text.toByteArray()).also {
+ runWriteAction {
+ deepest.createChildData(this, file.name).setBinaryContent(file.text.toByteArray())
+ }.also {
log.debug("Saved file ${file.name} under ${deepest.path}")
}
}
fun deleteFile(file: VirtualFile): Boolean {
return try {
- file.delete(this).let { true }
+ runWriteAction { file.delete(this) }.let { true }
} catch (e: IOException) {
false
}.also {
@@ -51,10 +55,12 @@ class FileManager(private val project: Project) {
// 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 {
+ runReadAction {
+ FilenameIndex.getVirtualFilesByName(
+ "components.json",
+ GlobalSearchScope.projectScope(project)
+ )
+ }.firstOrNull().also {
if (it == null) {
log.warn("components.json not found with the workaround")
}
@@ -64,10 +70,12 @@ class FileManager(private val project: Project) {
log.warn("No file named $name found with the workaround")
}
} else {
- FilenameIndex.getVirtualFilesByName(
- name,
- GlobalSearchScope.projectScope(project)
- )
+ runReadAction {
+ FilenameIndex.getVirtualFilesByName(
+ name,
+ GlobalSearchScope.projectScope(project)
+ )
+ }
}).filter { file ->
!file.path.contains("/node_modules/") && !file.path.contains("/.git/")
}.sortedBy { file ->
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt
new file mode 100644
index 0000000..d0615b8
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/helpers/PsiHelper.kt
@@ -0,0 +1,30 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.helpers
+
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.fileTypes.FileTypeManager
+import com.intellij.openapi.project.Project
+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 {
+ PsiFileFactory.getInstance(project).createFileFromText(
+ fileName, FileTypeManager.getInstance().getFileTypeByExtension(
+ fileName.substringAfterLast('.')
+ ), text
+ )
+ }
+ }
+
+ fun writeAction(file: PsiFile, description: String? = null, action: () -> Unit) {
+ WriteCommandAction.runWriteCommandAction(
+ file.project, description,
+ "com.github.warningimhack3r.intellijshadcnplugin",
+ action, file
+ )
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt
index 84ec460..4adbc54 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/Source.kt
@@ -2,17 +2,17 @@ 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.helpers.PsiHelper
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.application.runWriteAction
+import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.logger
-import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
-import com.intellij.psi.PsiFileFactory
+import com.intellij.psi.PsiFile
import kotlinx.serialization.KSerializer
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@@ -50,13 +50,9 @@ abstract class Source(val project: Project, private val serializer:
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
+ protected abstract fun adaptFileToConfig(file: PsiFile)
protected open fun fetchComponent(componentName: String): ComponentWithContents {
return RequestSender.sendRequest("$domain/registry/styles/${getLocalConfig().style}/$componentName.json")
@@ -101,6 +97,7 @@ abstract class Source(val project: Project, private val serializer:
}
open fun addComponent(componentName: String) {
+ val config = getLocalConfig()
// Install component
val component = fetchComponent(componentName)
val installedComponents = getInstalledComponents()
@@ -113,7 +110,7 @@ abstract class Source(val project: Project, private val serializer:
log.debug("Installing ${it.size} components: ${it.joinToString(", ") { component -> component.name }}")
}.forEach { downloadedComponent ->
val path =
- "${resolveAlias(getLocalConfig().aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) {
+ "${resolveAlias(config.aliases.components)}/${component.type.substringAfterLast(":")}" + if (usesDirectoriesForComponents()) {
"/${downloadedComponent.name}"
} else ""
// Check for deprecated components
@@ -134,7 +131,7 @@ abstract class Source(val project: Project, private val serializer:
listOf(
NotificationAction.createSimple("Remove " + if (multipleFiles) "them" else "it") {
remotelyDeletedFiles.forEach { file ->
- runWriteAction { fileManager.deleteFile(file) }
+ fileManager.deleteFile(file)
}
log.info(
"Removed deprecated file${if (multipleFiles) "s" else ""} from ${downloadedComponent.name} (${
@@ -157,13 +154,10 @@ abstract class Source(val project: Project, private val serializer:
}
}
downloadedComponent.files.forEach { file ->
- val psiFile = PsiFileFactory.getInstance(project).createFileFromText(
- adaptFileExtensionToConfig(file.name),
- FileTypeManager.getInstance().getFileTypeByExtension(
- adaptFileExtensionToConfig(file.name).substringAfterLast('.')
- ),
- adaptFileToConfig(file.content)
+ val psiFile = PsiHelper.createPsiFile(
+ project, adaptFileExtensionToConfig(file.name), file.content
)
+ adaptFileToConfig(psiFile)
fileManager.saveFileAtPath(psiFile, path)
}
}
@@ -205,14 +199,21 @@ abstract class Source(val project: Project, private val serializer:
open fun isComponentUpToDate(componentName: String): Boolean {
val remoteComponent = fetchComponent(componentName)
+ val componentPath =
+ "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${
+ if (usesDirectoriesForComponents()) {
+ "/${remoteComponent.name}"
+ } else ""
+ }"
+ val fileManager = FileManager(project)
return remoteComponent.files.all { file ->
- (FileManager(project).getFileContentsAtPath(
- "${resolveAlias(getLocalConfig().aliases.components)}/${remoteComponent.type.substringAfterLast(":")}${
- if (usesDirectoriesForComponents()) {
- "/${remoteComponent.name}"
- } else ""
- }/${file.name}"
- ) == adaptFileToConfig(file.content)).also {
+ val psiFile = PsiHelper.createPsiFile(
+ project, adaptFileExtensionToConfig(file.name), file.content
+ )
+ adaptFileToConfig(psiFile)
+ (fileManager.getFileContentsAtPath("$componentPath/${file.name}") == runReadAction {
+ psiFile.text
+ }).also {
log.debug("File ${file.name} for ${remoteComponent.name} is ${if (it) "" else "NOT "}up to date")
}
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/ReactConfig.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/ReactConfig.kt
index 43ec3e7..5ffd4cd 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/ReactConfig.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/ReactConfig.kt
@@ -14,11 +14,11 @@ import kotlinx.serialization.Serializable
@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
class ReactConfig(
- override val `$schema`: String,
+ override val `$schema`: String = "https://ui.shadcn.com/schema.json",
override val style: String,
- override val tailwind: Tailwind,
- val rsc: Boolean,
+ val rsc: Boolean = false,
val tsx: Boolean = true,
+ override val tailwind: Tailwind,
override val aliases: Aliases
) : Config() {
@@ -35,7 +35,7 @@ class ReactConfig(
override val config: String,
override val css: String,
override val baseColor: String,
- val cssVariables: Boolean,
+ val cssVariables: Boolean = true,
val prefix: String = ""
) : Config.Tailwind()
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SolidConfig.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SolidConfig.kt
index da4fcc9..697317e 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SolidConfig.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SolidConfig.kt
@@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
class SolidConfig(
- override val `$schema`: String,
+ override val `$schema`: String = "",
override val style: String,
override val tailwind: VueConfig.Tailwind,
override val aliases: Aliases
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SvelteConfig.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SvelteConfig.kt
index 092033e..3262850 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SvelteConfig.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/SvelteConfig.kt
@@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
@Serializable
class SvelteConfig(
- override val `$schema`: String,
+ override val `$schema`: String = "https://shadcn-svelte.com/schema.json",
override val style: String,
override val tailwind: Tailwind,
- val typescript: Boolean = true,
- override val aliases: Aliases
+ override val aliases: Aliases,
+ val typescript: Boolean = true
) : Config() {
@Serializable
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/VueConfig.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/VueConfig.kt
index 7925f34..8eca708 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/VueConfig.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/config/VueConfig.kt
@@ -12,12 +12,13 @@ import kotlinx.serialization.Serializable
* @param framework The Vue framework to use.
* @param aliases The aliases for the components and utils directories.
*/
-@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117")
+@Suppress("PROVIDED_RUNTIME_TOO_LOW", "kotlin:S117", "unused")
@Serializable
class VueConfig(
override val `$schema`: String = "https://shadcn-vue.com/schema.json",
override val style: String,
val typescript: Boolean = true,
+ val tsConfigPath: String = "./tsconfig.json",
override val tailwind: Tailwind,
val framework: Framework = Framework.VITE,
override val aliases: Aliases
@@ -35,7 +36,8 @@ class VueConfig(
override val config: String,
override val css: String,
override val baseColor: String,
- open val cssVariables: Boolean = true
+ val cssVariables: Boolean = true,
+ val prefix: String = ""
) : Config.Tailwind()
/**
@@ -61,5 +63,6 @@ class VueConfig(
class Aliases(
override val components: String,
override val utils: String,
+ val ui: String? = null
) : Config.Aliases()
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt
index 4a28702..eccf62d 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/ReactSource.kt
@@ -3,9 +3,13 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl
import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.FileManager
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config.ReactConfig
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ImportsPackagesReplacementVisitor
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.JSXClassReplacementVisitor
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ReactDirectiveRemovalVisitor
+import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
-import com.intellij.util.applyIf
+import com.intellij.psi.PsiFile
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@@ -48,135 +52,66 @@ class ReactSource(project: Project) : Source(project, ReactConfig.s
} else extension
}
- override fun adaptFileToConfig(contents: String): String {
+ override fun adaptFileToConfig(file: PsiFile) {
val config = getLocalConfig()
- // 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)
- )
- ) { result ->
- result.groupValues[0].replace(result.groupValues[1], config.aliases.utils)
- }.applyIf(config.rsc) {
- replace(
- Regex("\"use client\";*\n"), ""
- )
- }
- /**
- * Prepends `tw-` to all Tailwind classes.
- * @param classes The classes to prefix, an unquoted string of space-separated class names.
- * @param prefix The prefix to add to each class name.
- * @return The prefixed classes.
- */
- fun prefixClasses(classes: String, prefix: String): String = classes
- .split(" ")
- .filterNot { it.isEmpty() }
- .joinToString(" ") {
- val className = it.trim().split(":")
- if (className.size == 1) {
- "$prefix${className[0]}"
+ val importsPackagesReplacementVisitor = ImportsPackagesReplacementVisitor(project)
+ runReadAction { file.accept(importsPackagesReplacementVisitor) }
+ importsPackagesReplacementVisitor.replaceImports visitor@{ `package` ->
+ if (`package`.startsWith("@/registry/")) {
+ return@visitor if (config.aliases.ui != null) {
+ `package`.replace(Regex("^@/registry/[^/]+/ui"), config.aliases.ui)
} else {
- "${className.dropLast(1).joinToString(":")}:$prefix${className.last()}"
+ `package`.replace(
+ Regex("^@/registry/[^/]+"),
+ config.aliases.components,
+ )
}
+ } else if (`package` == "@/lib/utils") {
+ return@visitor config.aliases.utils
}
+ `package`
+ }
- /**
- * Converts CSS variables to Tailwind utility classes.
- * @param classes The classes to convert, an unquoted string of space-separated class names.
- * @param lightColors The light colors map to use.
- * @param darkColors The dark colors map to use.
- * @return The converted classes.
- */
- fun variablesToUtilities(
- classes: String,
- lightColors: Map,
- darkColors: Map
- ): String {
- // Note: this does not include `border` classes at the beginning or end of the string,
- // 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-css-vars.ts#L142-L145).
- val newClasses = classes.replace(" border ", " border border-border ")
-
- val prefixesToReplace = listOf("bg-", "text-", "border-", "ring-offset-", "ring-")
-
- /**
- * Replaces a class with CSS variables with Tailwind utility classes.
- * @param class The class to replace.
- * @return The replaced class.
- */
- fun replaceClass(`class`: String): String {
- val prefix = prefixesToReplace.find { `class`.startsWith(it) } ?: return `class`
- val color = `class`.substringAfter(prefix)
- val lightColor = lightColors[color]
- val darkColor = darkColors[color]
- return if (lightColor != null && darkColor != null) {
- "$prefix$lightColor dark:$prefix$darkColor"
- } else `class`
+ if (!config.rsc) {
+ val directiveVisitor = ReactDirectiveRemovalVisitor(project) { directive ->
+ directive == "use client"
}
-
- return newClasses
- .split(" ")
- .filterNot { it.isEmpty() }
- .joinToString(" ") {
- val className = it.trim().split(":")
- if (className.size == 1) {
- replaceClass(className[0])
- } else {
- "${className.dropLast(1).joinToString(":")}:${replaceClass(className.last())}"
- }
- }
+ runReadAction { file.accept(directiveVisitor) }
+ directiveVisitor.removeMatchingElements()
}
- fun handleClasses(classes: String): String {
- var newClasses = classes
- if (!config.tailwind.cssVariables) {
- val inlineColors = fetchColors().jsonObject["inlineColors"]?.jsonObject
- ?: throw Exception("Inline colors not found")
- newClasses = variablesToUtilities(
- newClasses,
- inlineColors.jsonObject["light"]?.jsonObject?.let { lightColors ->
- lightColors.keys.associateWith { lightColors[it]?.jsonPrimitive?.content ?: "" }
- } ?: emptyMap(),
- inlineColors.jsonObject["dark"]?.jsonObject?.let { darkColors ->
- darkColors.keys.associateWith { darkColors[it]?.jsonPrimitive?.content ?: "" }
- } ?: emptyMap()
- )
+ val prefixesToReplace = listOf("bg-", "text-", "border-", "ring-offset-", "ring-")
+
+ val inlineColors = fetchColors().jsonObject["inlineColors"]?.jsonObject
+ ?: throw Exception("Inline colors not found")
+ val lightColors = inlineColors.jsonObject["light"]?.jsonObject?.let { lightColors ->
+ lightColors.keys.associateWith { lightColors[it]?.jsonPrimitive?.content ?: "" }
+ } ?: emptyMap()
+ val darkColors = inlineColors.jsonObject["dark"]?.jsonObject?.let { darkColors ->
+ darkColors.keys.associateWith { darkColors[it]?.jsonPrimitive?.content ?: "" }
+ } ?: emptyMap()
+
+ val replacementVisitor = JSXClassReplacementVisitor(project)
+ runReadAction { file.accept(replacementVisitor) }
+ replacementVisitor.replaceClasses replacer@{ `class` ->
+ val modifier = if (`class`.contains(":")) `class`.substringBeforeLast(":") + ":" else ""
+ val className = `class`.substringAfterLast(":")
+ val twPrefix = config.tailwind.prefix
+ if (config.tailwind.cssVariables) {
+ return@replacer "$modifier$twPrefix$className"
}
- if (config.tailwind.prefix.isNotEmpty()) {
- newClasses = prefixClasses(newClasses, config.tailwind.prefix)
+ if (className == "border") {
+ return@replacer "$modifier${twPrefix}border $modifier${twPrefix}border-border"
}
- return newClasses
- }
-
- return Regex("className=(?:(?!>)[^\"'])*[\"']([^>]*)[\"']").replace(newContents) { result ->
- // matches any className, and takes everything inside the first quote to the last quote found before the closing `>`
- // if no quotes are found before the closing `>`, skips the match
- val match = result.groupValues[0]
- val group = result.groupValues[1]
- match.replace(
- group,
- // if the group contains a quote, we assume the classes are the last quoted string in the group
- if (group.contains("\"")) {
- group.substringBeforeLast('"') + "\"" + handleClasses(
- group.substringAfterLast('"')
- )
- } else if (group.contains("'")) {
- group.substringBeforeLast("'") + "'" + handleClasses(
- group.substringAfterLast("'")
- )
- } else handleClasses(group)
- )
+ val prefix = prefixesToReplace.find { className.startsWith(it) }
+ ?: return@replacer "$modifier$twPrefix$className"
+ val color = className.substringAfter(prefix)
+ val lightColor = lightColors[color]
+ val darkColor = darkColors[color]
+ if (lightColor != null && darkColor != null) {
+ "$modifier$twPrefix$prefix$lightColor dark:$modifier$twPrefix$prefix$darkColor"
+ } else "$modifier$twPrefix$className"
}
}
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt
index 5cb1b49..a206027 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SolidSource.kt
@@ -3,8 +3,12 @@ package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.impl
import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.FileManager
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config.SolidConfig
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ImportsPackagesReplacementVisitor
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.JSXClassReplacementVisitor
+import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiFile
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@@ -39,98 +43,47 @@ class SolidSource(project: Project) : Source(project, SolidConfig.s
}
}
- override fun adaptFileToConfig(contents: String): String {
+ override fun adaptFileToConfig(file: PsiFile) {
val config = getLocalConfig()
- // 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) {
- /**
- * Converts CSS variables to Tailwind utility classes.
- * @param classes The classes to convert, an unquoted string of space-separated class names.
- * @param lightColors The light colors map to use.
- * @param darkColors The dark colors map to use.
- * @return The converted classes.
- */
- fun variablesToUtilities(
- classes: String,
- lightColors: Map,
- darkColors: Map
- ): String {
- // Note: this does not include `border` classes at the beginning or end of the string,
- // but I'm once again following what the original code does for parity
- // (https://github.com/hngngn/shadcn-solid/blob/b808e0ecc9fd4689572d9fc0dfb7af81606a11f2/packages/cli/src/utils/transformers/transform-css-vars.ts#L144-L147).
- val newClasses = classes.replace(" border ", " border border-border ")
-
- val prefixesToReplace = listOf("bg-", "text-", "border-", "ring-offset-", "ring-")
-
- /**
- * Replaces a class with CSS variables with Tailwind utility classes.
- * @param class The class to replace.
- * @return The replaced class.
- */
- fun replaceClass(`class`: String): String {
- val prefix = prefixesToReplace.find { `class`.startsWith(it) } ?: return `class`
- val color = `class`.substringAfter(prefix)
- val lightColor = lightColors[color]
- val darkColor = darkColors[color]
- return if (lightColor != null && darkColor != null) {
- "$prefix$lightColor dark:$prefix$darkColor"
- } else `class`
- }
-
- return newClasses
- .split(" ")
- .filterNot { it.isEmpty() }
- .joinToString(" ") {
- val className = it.trim().split(":")
- if (className.size == 1) {
- replaceClass(className[0])
- } else {
- "${className.dropLast(1).joinToString(":")}:${replaceClass(className.last())}"
- }
- }
+ val importsPackagesReplacementVisitor = ImportsPackagesReplacementVisitor(project)
+ runReadAction { file.accept(importsPackagesReplacementVisitor) }
+ importsPackagesReplacementVisitor.replaceImports visitor@{ `package` ->
+ if (`package` == "@/libs/cn") {
+ return@visitor config.aliases.utils
}
+ `package`
+ }
- fun handleClasses(classes: String): String {
- val inlineColors = fetchColors().jsonObject["inlineColors"]?.jsonObject
- ?: throw Exception("Inline colors not found")
- return variablesToUtilities(
- classes,
- inlineColors.jsonObject["light"]?.jsonObject?.let { lightColors ->
- lightColors.keys.associateWith { lightColors[it]?.jsonPrimitive?.content ?: "" }
- } ?: emptyMap(),
- inlineColors.jsonObject["dark"]?.jsonObject?.let { darkColors ->
- darkColors.keys.associateWith { darkColors[it]?.jsonPrimitive?.content ?: "" }
- } ?: emptyMap()
- )
- }
+ if (!config.tailwind.cssVariables) {
+ val prefixesToReplace = listOf("bg-", "text-", "border-", "ring-offset-", "ring-")
+
+ val inlineColors = fetchColors().jsonObject["inlineColors"]?.jsonObject
+ ?: throw Exception("Inline colors not found")
+ val lightColors = inlineColors.jsonObject["light"]?.jsonObject?.let { lightColors ->
+ lightColors.keys.associateWith { lightColors[it]?.jsonPrimitive?.content ?: "" }
+ } ?: emptyMap()
+ val darkColors = inlineColors.jsonObject["dark"]?.jsonObject?.let { darkColors ->
+ darkColors.keys.associateWith { darkColors[it]?.jsonPrimitive?.content ?: "" }
+ } ?: emptyMap()
- Regex("className=(?:(?!>)[^\"'])*[\"']([^>]*)[\"']").replace(newContents) { result ->
- // matches any className, and takes everything inside the first quote to the last quote found before the closing `>`
- // if no quotes are found before the closing `>`, skips the match
- val match = result.groupValues[0]
- val group = result.groupValues[1]
- match.replace(
- group,
- // if the group contains a quote, we assume the classes are the last quoted string in the group
- if (group.contains("\"")) {
- group.substringBeforeLast('"') + "\"" + handleClasses(
- group.substringAfterLast('"')
- )
- } else if (group.contains("'")) {
- group.substringBeforeLast("'") + "'" + handleClasses(
- group.substringAfterLast("'")
- )
- } else handleClasses(group)
- )
+ val replacementVisitor = JSXClassReplacementVisitor(project)
+ runReadAction { file.accept(replacementVisitor) }
+ replacementVisitor.replaceClasses replacer@{ `class` ->
+ val modifier = if (`class`.contains(":")) `class`.substringBeforeLast(":") + ":" else ""
+ val className = `class`.substringAfterLast(":")
+ if (className == "border") {
+ return@replacer "${modifier}border ${modifier}border-border"
+ }
+ val prefix = prefixesToReplace.find { className.startsWith(it) }
+ ?: return@replacer "$modifier$className"
+ val color = className.substringAfter(prefix)
+ val lightColor = lightColors[color]
+ val darkColor = darkColors[color]
+ if (lightColor != null && darkColor != null) {
+ "$modifier$prefix$lightColor dark:$modifier$prefix$darkColor"
+ } else "$modifier$className"
}
- } else newContents
+ }
}
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt
index e5b77e6..7eb9950 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/SvelteSource.kt
@@ -7,10 +7,13 @@ import com.github.warningimhack3r.intellijshadcnplugin.backend.http.RequestSende
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config.SvelteConfig
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.ComponentWithContents
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ImportsPackagesReplacementVisitor
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 com.intellij.psi.PsiFile
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
@@ -81,12 +84,14 @@ class SvelteSource(project: Project) : Source(project, SvelteConfi
} else extension
}
- override fun adaptFileToConfig(contents: String): String {
+ override fun adaptFileToConfig(file: PsiFile) {
val config = getLocalConfig()
- return contents.replace(
- Regex("([\"'])[^\r\n/]+/registry/[^/]+"), "$1${cleanAlias(config.aliases.components)}"
- ).replace(
- "\$lib/utils", config.aliases.utils
- )
+ val importsPackagesReplacementVisitor = ImportsPackagesReplacementVisitor(project)
+ runReadAction { file.accept(importsPackagesReplacementVisitor) }
+ importsPackagesReplacementVisitor.replaceImports { `package` ->
+ `package`
+ .replace(Regex("^${'$'}lib/registry/[^/]+"), config.aliases.components)
+ .replace("\$lib/utils", config.aliases.utils)
+ }
}
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt
index abed128..aafb129 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/impl/VueSource.kt
@@ -4,11 +4,14 @@ import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.FileManag
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.config.VueConfig
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.remote.ComponentWithContents
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ImportsPackagesReplacementVisitor
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.VueClassReplacementVisitor
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 com.intellij.util.applyIf
+import com.intellij.psi.PsiFile
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@@ -60,116 +63,75 @@ class VueSource(project: Project) : Source(project, VueConfig.seriali
}
override fun adaptFileExtensionToConfig(extension: String): String {
- return if (!getLocalConfig().typescript) {
+ return if (getLocalConfig().typescript) extension else {
extension.replace(
Regex("\\.ts$"),
".js"
)
- } else extension
+ }
}
- override fun adaptFileToConfig(contents: String): String {
+ override fun adaptFileToConfig(file: PsiFile) {
val config = getLocalConfig()
- // 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/radix-vue/shadcn-vue/blob/9d9a6f929ce0f281b4af36161af80ed2bbdc4a16/packages/cli/src/utils/transformers/transform-import.ts#L19-L29).
- val newContents = Regex(".*\\{.*[ ,\n\t]+cn[ ,].*}.*\"(@/lib/cn).*").replace(
- contents.replace(
- Regex("@/registry/[^/]+"), cleanAlias(config.aliases.components)
- )
- ) { result ->
- result.groupValues[0].replace(result.groupValues[1], cleanAlias(config.aliases.utils))
- }.applyIf(!config.typescript) {
+ if (!config.typescript) {
NotificationManager(project).sendNotification(
"TypeScript option for Vue",
"You have TypeScript disabled in your shadcn/ui config. This feature is not supported yet. Please install/update your components with the CLI for now.",
NotificationType.WARNING
)
// TODO: detype Vue file
- this
}
- return if (!config.tailwind.cssVariables) {
- /**
- * Converts CSS variables to Tailwind utility classes.
- * @param classes The classes to convert, an unquoted string of space-separated class names.
- * @param lightColors The light colors map to use.
- * @param darkColors The dark colors map to use.
- * @return The converted classes.
- */
- fun variablesToUtilities(
- classes: String,
- lightColors: Map,
- darkColors: Map
- ): String {
- // Note: this does not include `border` classes at the beginning or end of the string,
- // but I'm once again following what the original code does for parity
- // (https://github.com/radix-vue/shadcn-vue/blob/4214134e1834fdabcc5f0354e11593360f076e8d/packages/cli/src/utils/transformers/transform-css-vars.ts#L87-L89).
- val newClasses = classes.replace(" border ", " border border-border ")
-
- val prefixesToReplace = listOf("bg-", "text-", "border-", "ring-offset-", "ring-")
- /**
- * Replaces a class with CSS variables with Tailwind utility classes.
- * @param class The class to replace.
- * @return The replaced class.
- */
- fun replaceClass(`class`: String): String {
- val prefix = prefixesToReplace.find { `class`.startsWith(it) } ?: return `class`
- val color = `class`.substringAfter(prefix)
- val lightColor = lightColors[color]
- val darkColor = darkColors[color]
- return if (lightColor != null && darkColor != null) {
- "$prefix$lightColor dark:$prefix$darkColor"
- } else `class`
+ val importsPackagesReplacementVisitor = ImportsPackagesReplacementVisitor(project)
+ runReadAction { file.accept(importsPackagesReplacementVisitor) }
+ importsPackagesReplacementVisitor.replaceImports replacer@{ `package` ->
+ if (`package`.startsWith("@/lib/registry/")) {
+ return@replacer if (config.aliases.ui != null) {
+ `package`.replace(Regex("^@/lib/registry/[^/]+/ui"), config.aliases.ui)
+ } else {
+ `package`.replace(
+ Regex("^@/lib/registry/[^/]+"),
+ config.aliases.components,
+ )
}
-
- return newClasses
- .split(" ")
- .filterNot { it.isEmpty() }
- .joinToString(" ") {
- val className = it.trim().split(":")
- if (className.size == 1) {
- replaceClass(className[0])
- } else {
- "${className.dropLast(1).joinToString(":")}:${replaceClass(className.last())}"
- }
- }
+ } else if (`package` == "@/lib/utils") {
+ return@replacer config.aliases.utils
}
+ `package`
+ }
- fun handleClasses(classes: String): String {
- val inlineColors = fetchColors().jsonObject["inlineColors"]?.jsonObject
- ?: throw Exception("Inline colors not found")
- return variablesToUtilities(
- classes,
- inlineColors.jsonObject["light"]?.jsonObject?.let { lightColors ->
- lightColors.keys.associateWith { lightColors[it]?.jsonPrimitive?.content ?: "" }
- } ?: emptyMap(),
- inlineColors.jsonObject["dark"]?.jsonObject?.let { darkColors ->
- darkColors.keys.associateWith { darkColors[it]?.jsonPrimitive?.content ?: "" }
- } ?: emptyMap()
- )
+ val prefixesToReplace = listOf("bg-", "text-", "border-", "ring-offset-", "ring-")
+
+ val inlineColors = fetchColors().jsonObject["inlineColors"]?.jsonObject
+ ?: throw Exception("Inline colors not found")
+ val lightColors = inlineColors.jsonObject["light"]?.jsonObject?.let { lightColors ->
+ lightColors.keys.associateWith { lightColors[it]?.jsonPrimitive?.content ?: "" }
+ } ?: emptyMap()
+ val darkColors = inlineColors.jsonObject["dark"]?.jsonObject?.let { darkColors ->
+ darkColors.keys.associateWith { darkColors[it]?.jsonPrimitive?.content ?: "" }
+ } ?: emptyMap()
+
+ val classReplacementVisitor = VueClassReplacementVisitor(project)
+ runReadAction { file.accept(classReplacementVisitor) }
+ classReplacementVisitor.replaceClasses replacer@{ `class` ->
+ val modifier = if (`class`.contains(":")) `class`.substringBeforeLast(":") + ":" else ""
+ val className = `class`.substringAfterLast(":")
+ val twPrefix = config.tailwind.prefix
+ if (config.tailwind.cssVariables) {
+ return@replacer "$modifier$twPrefix$className"
}
-
- val notTemplateClasses =
- Regex("[^:]class=(?:(?!>)[^\"'])*[\"']([^\"']*)[\"']").replace(newContents) { result ->
- result.groupValues[0].replace(
- result.groupValues[1],
- handleClasses(result.groupValues[1])
- )
- }
- // Double quoted templates
- Regex(":class=(?:(?!>)[^\"])*\"([^\"]*)\"").replace(notTemplateClasses) { result ->
- val group = result.groupValues[1]
- result.groupValues[0].replace(
- group,
- handleClasses(group
- .replace("\n", " ")
- .split(", ")
- .map { it.trim() }
- .last { it.startsWith("'") || it.endsWith("'") })
- )
+ if (className == "border") {
+ return@replacer "$modifier${twPrefix}border $modifier${twPrefix}border-border"
}
- } else newContents
+ val prefix = prefixesToReplace.find { className.startsWith(it) }
+ ?: return@replacer "$modifier$twPrefix$className"
+ val color = className.substringAfter(prefix)
+ val lightColor = lightColors[color]
+ val darkColor = darkColors[color]
+ if (lightColor != null && darkColor != null) {
+ "$modifier$twPrefix$prefix$lightColor dark:$modifier$twPrefix$prefix$darkColor"
+ } else "$modifier$twPrefix$className"
+ }
}
override fun getRegistryDependencies(component: ComponentWithContents): List {
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ClassReplacementVisitor.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ClassReplacementVisitor.kt
new file mode 100644
index 0000000..49b96b0
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ClassReplacementVisitor.kt
@@ -0,0 +1,126 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement
+
+import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.PsiHelper
+import com.intellij.lang.javascript.JSStubElementTypes
+import com.intellij.lang.javascript.psi.JSProperty
+import com.intellij.lang.javascript.psi.impl.JSPsiElementFactory
+import com.intellij.lang.typescript.TypeScriptStubElementTypes
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.patterns.PsiElementPattern
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiRecursiveElementVisitor
+import com.intellij.psi.SmartPointerManager
+import com.intellij.psi.SmartPsiElementPointer
+import com.intellij.psi.xml.XmlTokenType
+
+abstract class ClassReplacementVisitor(project: Project) : PsiRecursiveElementVisitor() {
+ private val matchingElements = mutableListOf>()
+ private val smartPointerManager = SmartPointerManager.getInstance(project)
+
+ abstract val attributePattern: PsiElementPattern.Capture
+ abstract val attributeValuePattern: PsiElementPattern.Capture
+
+ private val attributeValueTokenPattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)
+ private val jsLiteralExpressionPattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(JSStubElementTypes.LITERAL_EXPRESSION)
+ private val jsCallExpressionPattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(JSStubElementTypes.CALL_EXPRESSION)
+ private val jsOrTsVariablePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement().andOr(
+ PlatformPatterns.psiElement(TypeScriptStubElementTypes.TYPESCRIPT_VARIABLE),
+ PlatformPatterns.psiElement(JSStubElementTypes.VARIABLE)
+ )
+
+ abstract fun classAttributeNameFromElement(element: PsiElement): String?
+
+ private fun depthRecurseChildren(element: PsiElement, action: (PsiElement) -> Unit) {
+ element.children.forEach { child ->
+ action(child)
+ depthRecurseChildren(child, action)
+ }
+ }
+
+ private fun saveMatchingElement(psiElement: PsiElement) {
+ matchingElements.add(smartPointerManager.createSmartPsiElementPointer(psiElement, psiElement.containingFile))
+ }
+
+ override fun visitElement(element: PsiElement) {
+ super.visitElement(element)
+
+ when {
+ attributeValuePattern
+ .withChild(attributeValueTokenPattern)
+ .withAncestor(2, attributePattern)
+ .accepts(element) -> {
+ // Regular string attribute value
+ val attributeName = classAttributeNameFromElement(element.parent) ?: return
+ if (attributeName != "className" && attributeName != "class") return
+ saveMatchingElement(element)
+ }
+
+ jsLiteralExpressionPattern
+ .withAncestor(2, jsCallExpressionPattern)
+ .withAncestor(5, attributeValuePattern)
+ .withAncestor(6, attributePattern)
+ .accepts(element) -> {
+ // String literal directly inside a function call
+ saveMatchingElement(element)
+ }
+
+ jsCallExpressionPattern
+ .withChild(
+ PlatformPatterns.psiElement().andOr(
+ PlatformPatterns.psiElement().withText("tv"),
+ PlatformPatterns.psiElement().withText("cva")
+ )
+ )
+ .withParent(jsOrTsVariablePattern)
+ .accepts(element) -> {
+ // tailwind-variants & class-variance-authority
+ depthRecurseChildren(element) { child ->
+ if (jsLiteralExpressionPattern.accepts(child)) {
+ val greatGrandparent = child.parent?.parent?.parent ?: return@depthRecurseChildren
+ if ((greatGrandparent is JSProperty && greatGrandparent.name != "defaultVariants")
+ || greatGrandparent !is JSProperty
+ ) {
+ saveMatchingElement(child)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Suppress("UnstableApiUsage")
+ fun replaceClasses(newText: (String) -> String) {
+ matchingElements.forEach { element ->
+ val psiElement = runReadAction { element.dereference() } ?: return@forEach
+ replaceClassName(psiElement, newText)
+ }
+ }
+
+ private fun replaceClassName(element: PsiElement, newText: (String) -> String) {
+ val text = runReadAction { element.text }
+ val quote = when (text.first()) {
+ '\'', '`', '"' -> text.first().toString()
+ else -> ""
+ }
+ val classes = text
+ .split(" ")
+ .filter { it.isNotEmpty() }
+ .joinToString(" ") {
+ if (quote.isEmpty()) {
+ newText(it)
+ } else newText(it.replace(Regex(quote), ""))
+ }
+ val newElement = runReadAction {
+ JSPsiElementFactory.createJSExpression("$quote$classes$quote", element)
+ }
+ PsiHelper.writeAction(runReadAction { element.containingFile }) {
+ element.replace(newElement)
+ }
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ImportsPackagesReplacementVisitor.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ImportsPackagesReplacementVisitor.kt
new file mode 100644
index 0000000..1922258
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ImportsPackagesReplacementVisitor.kt
@@ -0,0 +1,57 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement
+
+import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.PsiHelper
+import com.intellij.lang.ecmascript6.ES6StubElementTypes
+import com.intellij.lang.javascript.JSTokenTypes
+import com.intellij.lang.javascript.psi.impl.JSPsiElementFactory
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiRecursiveElementVisitor
+import com.intellij.psi.SmartPointerManager
+import com.intellij.psi.SmartPsiElementPointer
+
+class ImportsPackagesReplacementVisitor(project: Project) : PsiRecursiveElementVisitor() {
+ private val matchingElements = mutableListOf>()
+ private val smartPointerManager = SmartPointerManager.getInstance(project)
+
+ override fun visitElement(element: PsiElement) {
+ super.visitElement(element)
+
+ if (PlatformPatterns.psiElement(JSTokenTypes.STRING_LITERAL)
+ .withParent(PlatformPatterns.psiElement(ES6StubElementTypes.FROM_CLAUSE))
+ .accepts(element)
+ ) {
+ matchingElements.add(smartPointerManager.createSmartPsiElementPointer(element, element.containingFile))
+ }
+ }
+
+ @Suppress("UnstableApiUsage")
+ fun replaceImports(newText: (String) -> String) {
+ matchingElements.forEach { element ->
+ val psiElement = runReadAction { element.dereference() } ?: return@forEach
+ replaceImport(psiElement, newText)
+ }
+ }
+
+ private fun replaceImport(element: PsiElement, newText: (String) -> String) {
+ val text = runReadAction { element.text }
+ val quote = when (text.first()) {
+ '\'', '`', '"' -> text.first().toString()
+ else -> ""
+ }
+ val newImport = text.let {
+ if (quote.isEmpty()) {
+ // Cannot happen, but just in case
+ newText(it)
+ } else newText(it.replace(Regex(quote), ""))
+ }
+ val newElement = runReadAction {
+ JSPsiElementFactory.createJSExpression("$quote$newImport$quote", element)
+ }
+ PsiHelper.writeAction(runReadAction { element.containingFile }) {
+ element.replace(newElement)
+ }
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/JSXClassReplacementVisitor.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/JSXClassReplacementVisitor.kt
new file mode 100644
index 0000000..132801c
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/JSXClassReplacementVisitor.kt
@@ -0,0 +1,22 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement
+
+import com.intellij.lang.javascript.JSStubElementTypes
+import com.intellij.lang.javascript.psi.e4x.impl.JSXmlAttributeImpl
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.patterns.PsiElementPattern
+import com.intellij.psi.PsiElement
+import com.intellij.psi.xml.XmlElementType
+
+class JSXClassReplacementVisitor(project: Project) : ClassReplacementVisitor(project) {
+ override val attributePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(JSStubElementTypes.XML_ATTRIBUTE)
+ override val attributeValuePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(XmlElementType.XML_ATTRIBUTE_VALUE)
+
+ @Suppress("UnstableApiUsage")
+ override fun classAttributeNameFromElement(element: PsiElement): String? {
+ val attribute = element as? JSXmlAttributeImpl ?: return null
+ return attribute.name
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ReactDirectiveRemovalVisitor.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ReactDirectiveRemovalVisitor.kt
new file mode 100644
index 0000000..f42f3af
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/ReactDirectiveRemovalVisitor.kt
@@ -0,0 +1,59 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement
+
+import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.PsiHelper
+import com.intellij.lang.javascript.JSElementTypes
+import com.intellij.lang.javascript.JSStubElementTypes
+import com.intellij.lang.javascript.JSTokenTypes
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.psi.*
+
+class ReactDirectiveRemovalVisitor(
+ project: Project,
+ val directiveValue: (String) -> Boolean
+) : PsiRecursiveElementVisitor() {
+ private val matchingElements = mutableListOf>()
+ private val smartPointerManager = SmartPointerManager.getInstance(project)
+
+ private val elementsToRemove = PlatformPatterns.psiElement().andOr(
+ PlatformPatterns.psiElement(JSTokenTypes.SEMICOLON),
+ PlatformPatterns.psiElement(CustomHighlighterTokenType.WHITESPACE)
+ // TODO: Find a way to remove newlines?
+ )
+
+ private var directiveFound = false
+ private var done = false
+
+ override fun visitElement(element: PsiElement) {
+ super.visitElement(element)
+
+ if (!done) {
+ val isDirectiveCandidate = PlatformPatterns.psiElement(JSElementTypes.EXPRESSION_STATEMENT)
+ .withChild(PlatformPatterns.psiElement(JSStubElementTypes.LITERAL_EXPRESSION))
+ .accepts(element)
+ val isJunk = elementsToRemove.accepts(element)
+
+ if (!directiveFound && isDirectiveCandidate && directiveValue(element.text.replace(Regex("['\";]"), ""))) {
+ matchingElements.add(smartPointerManager.createSmartPsiElementPointer(element))
+ directiveFound = true
+ } else if (directiveFound) {
+ if (isJunk) {
+ matchingElements.add(smartPointerManager.createSmartPsiElementPointer(element))
+ } else {
+ done = true
+ }
+ }
+ }
+ }
+
+ @Suppress("UnstableApiUsage")
+ fun removeMatchingElements() {
+ matchingElements.forEach { element ->
+ val psiElement = runReadAction { element.dereference() } ?: return
+ PsiHelper.writeAction(runReadAction { psiElement.containingFile }) {
+ psiElement.delete()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/SvelteClassReplacementVisitor.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/SvelteClassReplacementVisitor.kt
new file mode 100644
index 0000000..7f6a3cc
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/SvelteClassReplacementVisitor.kt
@@ -0,0 +1,21 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement
+
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.patterns.PsiElementPattern
+import com.intellij.psi.PsiElement
+import com.intellij.psi.xml.XmlElementType
+import dev.blachut.svelte.lang.psi.SvelteHtmlAttribute
+import dev.blachut.svelte.lang.psi.SvelteHtmlElementTypes
+
+class SvelteClassReplacementVisitor(project: Project) : ClassReplacementVisitor(project) {
+ override val attributePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(SvelteHtmlElementTypes.SVELTE_HTML_ATTRIBUTE)
+ override val attributeValuePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(XmlElementType.XML_ATTRIBUTE_VALUE)
+
+ override fun classAttributeNameFromElement(element: PsiElement): String? {
+ val attribute = element as? SvelteHtmlAttribute ?: return null
+ return attribute.name
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/VueClassReplacementVisitor.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/VueClassReplacementVisitor.kt
new file mode 100644
index 0000000..ae47620
--- /dev/null
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/backend/sources/replacement/VueClassReplacementVisitor.kt
@@ -0,0 +1,20 @@
+package com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement
+
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.patterns.PsiElementPattern
+import com.intellij.psi.PsiElement
+import com.intellij.psi.impl.source.xml.XmlAttributeImpl
+import com.intellij.psi.xml.XmlElementType
+
+class VueClassReplacementVisitor(project: Project) : ClassReplacementVisitor(project) {
+ override val attributePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(XmlElementType.XML_ATTRIBUTE)
+ override val attributeValuePattern: PsiElementPattern.Capture =
+ PlatformPatterns.psiElement(XmlElementType.XML_ATTRIBUTE_VALUE)
+
+ override fun classAttributeNameFromElement(element: PsiElement): String? {
+ val attribute = element as? XmlAttributeImpl ?: return null
+ return attribute.name
+ }
+}
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt
index 75d669b..d5fe784 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPPanelPopulator.kt
@@ -2,7 +2,6 @@ package com.github.warningimhack3r.intellijshadcnplugin.ui
import com.github.warningimhack3r.intellijshadcnplugin.backend.SourceScanner
import com.github.warningimhack3r.intellijshadcnplugin.backend.helpers.FileManager
-import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.ui.components.JBLabel
@@ -22,11 +21,10 @@ class ISPPanelPopulator(private val project: Project) {
fun populateToolWindowPanel(panel: JComponent) {
log.info("Initializing tool window content")
CoroutineScope(SupervisorJob() + Dispatchers.Default).async {
- return@async Pair(runReadAction {
- SourceScanner.findShadcnImplementation(project)
- }, runReadAction {
- FileManager(project).getVirtualFilesByName("package.json")
- }.size)
+ return@async Pair(
+ SourceScanner.findShadcnImplementation(project),
+ FileManager(project).getVirtualFilesByName("package.json").size
+ )
}.asCompletableFuture().thenApplyAsync { (source, packageJsonCount) ->
log.info("Shadcn implementation detected: $source, package.json count: $packageJsonCount")
panel.removeAll()
diff --git a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt
index 0da5028..9a3a151 100644
--- a/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ui/ISPWindowContents.kt
@@ -1,11 +1,10 @@
package com.github.warningimhack3r.intellijshadcnplugin.ui
import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.Source
-import com.intellij.openapi.application.runReadAction
-import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextField
+import com.intellij.util.SlowOperations
import com.intellij.util.ui.JBUI
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
@@ -52,7 +51,7 @@ class ISPWindowContents(private val source: Source<*>) {
val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
var installedComponents = emptyList()
coroutineScope.launch {
- installedComponents = runReadAction { source.getInstalledComponents() }
+ installedComponents = source.getInstalledComponents()
}.invokeOnCompletion { throwable ->
if (throwable != null && throwable !is CancellationException) {
return@invokeOnCompletion
@@ -60,7 +59,7 @@ class ISPWindowContents(private val source: Source<*>) {
// Add a component panel
add(createPanel("Add a component") {
coroutineScope.async {
- runReadAction { source.fetchAllComponents() }.map { component ->
+ source.fetchAllComponents().map { component ->
Item(
component.name,
"${
@@ -71,7 +70,9 @@ class ISPWindowContents(private val source: Source<*>) {
} component for ${source.framework}",
listOf(
LabeledAction("Add", CompletionAction.DISABLE_ROW) {
- runWriteAction { source.addComponent(component.name) }
+ SlowOperations.allowSlowOperations {
+ source.addComponent(component.name)
+ }
}
),
installedComponents.contains(component.name)
@@ -90,15 +91,15 @@ class ISPWindowContents(private val source: Source<*>) {
// Manage components panel
add(createPanel("Manage components", coroutineScope.async {
val shouldDisplay =
- runReadAction {
- installedComponents.any { component -> !source.isComponentUpToDate(component) }
- }
+ installedComponents.any { component -> !source.isComponentUpToDate(component) }
if (shouldDisplay) {
JButton("Update all").apply {
addActionListener {
isEnabled = false
- installedComponents.forEach { component ->
- runWriteAction { source.addComponent(component) }
+ SlowOperations.allowSlowOperations {
+ installedComponents.forEach { component ->
+ source.addComponent(component)
+ }
}
// TODO: Update the list's row actions
val par = parent
@@ -116,12 +117,16 @@ class ISPWindowContents(private val source: Source<*>) {
null,
listOfNotNull(
LabeledAction("Update", CompletionAction.REMOVE_TRIGGER) {
- runWriteAction { source.addComponent(component) }
+ SlowOperations.allowSlowOperations {
+ source.addComponent(component)
+ }
}.takeIf {
- runReadAction { !source.isComponentUpToDate(component) }
+ !source.isComponentUpToDate(component)
},
LabeledAction("Remove", CompletionAction.REMOVE_ROW) {
- runWriteAction { source.removeComponent(component) }
+ SlowOperations.allowSlowOperations {
+ source.removeComponent(component)
+ }
}
)
)
diff --git a/src/main/resources/META-INF/com.github.warningimhack3r.intellijshadcnplugin-withSvelte.xml b/src/main/resources/META-INF/com.github.warningimhack3r.intellijshadcnplugin-withSvelte.xml
new file mode 100644
index 0000000..e9f4a7d
--- /dev/null
+++ b/src/main/resources/META-INF/com.github.warningimhack3r.intellijshadcnplugin-withSvelte.xml
@@ -0,0 +1 @@
+
diff --git a/src/main/resources/META-INF/com.github.warningimhack3r.intellijshadcnplugin-withVue.xml b/src/main/resources/META-INF/com.github.warningimhack3r.intellijshadcnplugin-withVue.xml
new file mode 100644
index 0000000..e9f4a7d
--- /dev/null
+++ b/src/main/resources/META-INF/com.github.warningimhack3r.intellijshadcnplugin-withVue.xml
@@ -0,0 +1 @@
+
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 9c8493f..6a83ba4 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -5,6 +5,12 @@
WarningImHack3r
com.intellij.modules.platform
+
+ dev.blachut.svelte.lang
+
+
+ org.jetbrains.plugins.vue
+
): ClassReplacementVisitor {
+ val constructor = visitorClass.primaryConstructor
+ if (constructor != null && constructor.parameters.size == 1) {
+ return constructor.call(project)
+ } else {
+ throw IllegalArgumentException("Invalid visitor class. It should have a primary constructor with 1 parameter.")
+ }
+ }
+
+ private fun beforeAndAfterContents(fileName: String): Pair {
+ val file = myFixture.configureByFile(fileName)
+ val visitorClass = when (val extension = file.name.substringAfterLast('.')) {
+ "jsx", "tsx", "js", "ts" -> JSXClassReplacementVisitor::class
+ "svelte" -> SvelteClassReplacementVisitor::class
+ "vue" -> VueClassReplacementVisitor::class
+ else -> throw IllegalArgumentException("Unsupported extension: $extension")
+ }
+ val visitor = createVisitor(visitorClass)
+ file.accept(visitor)
+ visitor.replaceClasses { "a-$it" }
+ return Pair(myFixture.configureByFile(fileName.let {
+ it.substringBeforeLast('.') + "_after." + it.substringAfterLast('.')
+ }).text, file.text)
+ }
+
+ fun testBasicClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("basic.tsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testSingleQuotesClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("singleQuotes.tsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testSvelteClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("basic.svelte")
+ assertEquals(expected, actual)
+ }
+
+ fun testVueClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("basic.vue")
+ assertEquals(expected, actual)
+ }
+
+ fun testNestedAndOtherAttributesReplacement() {
+ val (expected, actual) = beforeAndAfterContents("nestedAndOtherAttributes.jsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testOtherAttributeReplacement() {
+ val (expected, actual) = beforeAndAfterContents("simpleOtherAttribute.jsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullSimpleSvelteComponentClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullSimple.svelte")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullComplexSvelteComponentClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullComplex.svelte")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullSvelteVueTSClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullSvelteVue.ts")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullSvelteVueJSClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullSvelteVue.js")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullSimpleVueComponentClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullSimple.vue")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullComplexVueComponentClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullComplex.vue")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullSimpleJSXComponentClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullSimple.tsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testFullComplexJSXComponentClassReplacement() {
+ val (expected, actual) = beforeAndAfterContents("fullComplex.tsx")
+ assertEquals(expected, actual)
+ }
+}
diff --git a/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/DummyTests.kt b/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/DummyTests.kt
deleted file mode 100644
index f81db28..0000000
--- a/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/DummyTests.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.github.warningimhack3r.intellijshadcnplugin
-
-import com.intellij.testFramework.fixtures.BasePlatformTestCase
-
-class DummyTests : BasePlatformTestCase() {
- fun testDummy() {
- assertTrue(true)
- }
-}
diff --git a/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ImportsReplacementTests.kt b/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ImportsReplacementTests.kt
new file mode 100644
index 0000000..739a724
--- /dev/null
+++ b/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ImportsReplacementTests.kt
@@ -0,0 +1,61 @@
+package com.github.warningimhack3r.intellijshadcnplugin
+
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ImportsPackagesReplacementVisitor
+import com.intellij.testFramework.TestDataPath
+import com.intellij.testFramework.fixtures.BasePlatformTestCase
+
+@TestDataPath("\$CONTENT_ROOT/src/test/testData")
+class ImportsReplacementTests : BasePlatformTestCase() {
+
+ override fun getTestDataPath() = "src/test/testData/importsReplacement"
+
+ private fun beforeAndAfterContents(fileName: String): Pair {
+ val file = myFixture.configureByFile(fileName)
+ val visitor = ImportsPackagesReplacementVisitor(project)
+ file.accept(visitor)
+ visitor.replaceImports { "a-$it" }
+ return Pair(myFixture.configureByFile(fileName.let {
+ it.substringBeforeLast('.') + "_after." + it.substringAfterLast('.')
+ }).text, file.text)
+ }
+
+ fun testJSImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("imports.js")
+ assertEquals(expected, actual)
+ }
+
+ fun testTSImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("imports.ts")
+ assertEquals(expected, actual)
+ }
+
+ fun testJSXImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("imports.jsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testTSXImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("imports.tsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testSvelteTSImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("importsTS.svelte")
+ assertEquals(expected, actual)
+ }
+
+ fun testSvelteJSImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("importsJS.svelte")
+ assertEquals(expected, actual)
+ }
+
+ fun testVueTSImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("importsTS.vue")
+ assertEquals(expected, actual)
+ }
+
+ fun testVueJSImportsReplacement() {
+ val (expected, actual) = beforeAndAfterContents("importsJS.vue")
+ assertEquals(expected, actual)
+ }
+}
diff --git a/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ReactDirectiveRemovalTests.kt b/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ReactDirectiveRemovalTests.kt
new file mode 100644
index 0000000..12e8624
--- /dev/null
+++ b/src/test/kotlin/com/github/warningimhack3r/intellijshadcnplugin/ReactDirectiveRemovalTests.kt
@@ -0,0 +1,31 @@
+package com.github.warningimhack3r.intellijshadcnplugin
+
+import com.github.warningimhack3r.intellijshadcnplugin.backend.sources.replacement.ReactDirectiveRemovalVisitor
+import com.intellij.testFramework.TestDataPath
+import com.intellij.testFramework.fixtures.BasePlatformTestCase
+
+@TestDataPath("\$CONTENT_ROOT/src/test/testData")
+class ReactDirectiveRemovalTests : BasePlatformTestCase() {
+
+ override fun getTestDataPath() = "src/test/testData/reactDirectiveRemoval"
+
+ private fun beforeAndAfterContents(fileName: String): Pair {
+ val file = myFixture.configureByFile(fileName)
+ val visitor = ReactDirectiveRemovalVisitor(project) { true }
+ file.accept(visitor)
+ visitor.removeMatchingElements()
+ return Pair(myFixture.configureByFile(fileName.let {
+ it.substringBeforeLast('.') + "_after." + it.substringAfterLast('.')
+ }).text, file.text)
+ }
+
+ fun testJSDirectiveRemoval() {
+ val (expected, actual) = beforeAndAfterContents("react.jsx")
+ assertEquals(expected, actual)
+ }
+
+ fun testTSDirectiveRemoval() {
+ val (expected, actual) = beforeAndAfterContents("react.tsx")
+ assertEquals(expected, actual)
+ }
+}
diff --git a/src/test/testData/classReplacement/basic.svelte b/src/test/testData/classReplacement/basic.svelte
new file mode 100644
index 0000000..7fe7e6e
--- /dev/null
+++ b/src/test/testData/classReplacement/basic.svelte
@@ -0,0 +1,5 @@
+
+
+Hello {name}!
diff --git a/src/test/testData/classReplacement/basic.tsx b/src/test/testData/classReplacement/basic.tsx
new file mode 100644
index 0000000..4f1a554
--- /dev/null
+++ b/src/test/testData/classReplacement/basic.tsx
@@ -0,0 +1 @@
+const tag = Hello, world!
;
diff --git a/src/test/testData/classReplacement/basic.vue b/src/test/testData/classReplacement/basic.vue
new file mode 100644
index 0000000..350e204
--- /dev/null
+++ b/src/test/testData/classReplacement/basic.vue
@@ -0,0 +1,15 @@
+
+
+ {{ message }}
+
+
+
+
diff --git a/src/test/testData/classReplacement/basic_after.svelte b/src/test/testData/classReplacement/basic_after.svelte
new file mode 100644
index 0000000..4bcd7b2
--- /dev/null
+++ b/src/test/testData/classReplacement/basic_after.svelte
@@ -0,0 +1,5 @@
+
+
+Hello {name}!
diff --git a/src/test/testData/classReplacement/basic_after.tsx b/src/test/testData/classReplacement/basic_after.tsx
new file mode 100644
index 0000000..18fb0d3
--- /dev/null
+++ b/src/test/testData/classReplacement/basic_after.tsx
@@ -0,0 +1 @@
+const tag = Hello, world!
;
diff --git a/src/test/testData/classReplacement/basic_after.vue b/src/test/testData/classReplacement/basic_after.vue
new file mode 100644
index 0000000..e1b8059
--- /dev/null
+++ b/src/test/testData/classReplacement/basic_after.vue
@@ -0,0 +1,15 @@
+
+
+ {{ message }}
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullComplex.svelte b/src/test/testData/classReplacement/fullComplex.svelte
new file mode 100644
index 0000000..c2cefb9
--- /dev/null
+++ b/src/test/testData/classReplacement/fullComplex.svelte
@@ -0,0 +1,26 @@
+
+
+
+ svg]:rotate-180",
+ className
+ )}
+ {...$$restProps}
+ on:click
+ >
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullComplex.tsx b/src/test/testData/classReplacement/fullComplex.tsx
new file mode 100644
index 0000000..bd82d05
--- /dev/null
+++ b/src/test/testData/classReplacement/fullComplex.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/registry/default/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/src/test/testData/classReplacement/fullComplex.vue b/src/test/testData/classReplacement/fullComplex.vue
new file mode 100644
index 0000000..42b8267
--- /dev/null
+++ b/src/test/testData/classReplacement/fullComplex.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullComplex_after.svelte b/src/test/testData/classReplacement/fullComplex_after.svelte
new file mode 100644
index 0000000..053196e
--- /dev/null
+++ b/src/test/testData/classReplacement/fullComplex_after.svelte
@@ -0,0 +1,26 @@
+
+
+
+ svg]:rotate-180",
+ className
+ )}
+ {...$$restProps}
+ on:click
+ >
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullComplex_after.tsx b/src/test/testData/classReplacement/fullComplex_after.tsx
new file mode 100644
index 0000000..0ceceab
--- /dev/null
+++ b/src/test/testData/classReplacement/fullComplex_after.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/registry/default/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+ }: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({className, ...props}, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/src/test/testData/classReplacement/fullComplex_after.vue b/src/test/testData/classReplacement/fullComplex_after.vue
new file mode 100644
index 0000000..bc61ebc
--- /dev/null
+++ b/src/test/testData/classReplacement/fullComplex_after.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullSimple.svelte b/src/test/testData/classReplacement/fullSimple.svelte
new file mode 100644
index 0000000..91cdbc7
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSimple.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullSimple.tsx b/src/test/testData/classReplacement/fullSimple.tsx
new file mode 100644
index 0000000..32b3e7c
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSimple.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({className, variant, size, asChild = false, ...props}, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export {Button, buttonVariants}
diff --git a/src/test/testData/classReplacement/fullSimple.vue b/src/test/testData/classReplacement/fullSimple.vue
new file mode 100644
index 0000000..5bb5b60
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSimple.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullSimple_after.svelte b/src/test/testData/classReplacement/fullSimple_after.svelte
new file mode 100644
index 0000000..44aad4d
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSimple_after.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullSimple_after.tsx b/src/test/testData/classReplacement/fullSimple_after.tsx
new file mode 100644
index 0000000..bb06b46
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSimple_after.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "a-inline-flex a-items-center a-justify-center a-whitespace-nowrap a-rounded-md a-text-sm a-font-medium a-ring-offset-background a-transition-colors a-focus-visible:outline-none a-focus-visible:ring-2 a-focus-visible:ring-ring a-focus-visible:ring-offset-2 a-disabled:pointer-events-none a-disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "a-bg-primary a-text-primary-foreground a-hover:bg-primary/90",
+ destructive:
+ "a-bg-destructive a-text-destructive-foreground a-hover:bg-destructive/90",
+ outline:
+ "a-border a-border-input a-bg-background a-hover:bg-accent a-hover:text-accent-foreground",
+ secondary:
+ "a-bg-secondary a-text-secondary-foreground a-hover:bg-secondary/80",
+ ghost: "a-hover:bg-accent a-hover:text-accent-foreground",
+ link: "a-text-primary a-underline-offset-4 a-hover:underline",
+ },
+ size: {
+ default: "a-h-10 a-px-4 a-py-2",
+ sm: "a-h-9 a-rounded-md a-px-3",
+ lg: "a-h-11 a-rounded-md a-px-8",
+ icon: "a-h-10 a-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({className, variant, size, asChild = false, ...props}, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export {Button, buttonVariants}
diff --git a/src/test/testData/classReplacement/fullSimple_after.vue b/src/test/testData/classReplacement/fullSimple_after.vue
new file mode 100644
index 0000000..dd4cf24
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSimple_after.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/src/test/testData/classReplacement/fullSvelteVue.js b/src/test/testData/classReplacement/fullSvelteVue.js
new file mode 100644
index 0000000..03d272e
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSvelteVue.js
@@ -0,0 +1,34 @@
+import { tv } from "tailwind-variants";
+import Root from "./button.svelte";
+
+const buttonVariants = tv({
+ base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+});
+
+export {
+ Root,
+ //
+ Root as Button,
+ buttonVariants,
+};
diff --git a/src/test/testData/classReplacement/fullSvelteVue.ts b/src/test/testData/classReplacement/fullSvelteVue.ts
new file mode 100644
index 0000000..99016e6
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSvelteVue.ts
@@ -0,0 +1,49 @@
+import { tv, type VariantProps } from "tailwind-variants";
+import type { Button as ButtonPrimitive } from "bits-ui";
+import Root from "./button.svelte";
+
+const buttonVariants = tv({
+ base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+});
+
+type Variant = VariantProps["variant"];
+type Size = VariantProps["size"];
+
+type Props = ButtonPrimitive.Props & {
+ variant?: Variant;
+ size?: Size;
+};
+
+type Events = ButtonPrimitive.Events;
+
+export {
+ Root,
+ type Props,
+ type Events,
+ //
+ Root as Button,
+ type Props as ButtonProps,
+ type Events as ButtonEvents,
+ buttonVariants,
+};
diff --git a/src/test/testData/classReplacement/fullSvelteVue_after.js b/src/test/testData/classReplacement/fullSvelteVue_after.js
new file mode 100644
index 0000000..e78d49e
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSvelteVue_after.js
@@ -0,0 +1,34 @@
+import { tv } from "tailwind-variants";
+import Root from "./button.svelte";
+
+const buttonVariants = tv({
+ base: "a-inline-flex a-items-center a-justify-center a-whitespace-nowrap a-rounded-md a-text-sm a-font-medium a-ring-offset-background a-transition-colors a-focus-visible:outline-none a-focus-visible:ring-2 a-focus-visible:ring-ring a-focus-visible:ring-offset-2 a-disabled:pointer-events-none a-disabled:opacity-50",
+ variants: {
+ variant: {
+ default: "a-bg-primary a-text-primary-foreground a-hover:bg-primary/90",
+ destructive: "a-bg-destructive a-text-destructive-foreground a-hover:bg-destructive/90",
+ outline:
+ "a-border a-border-input a-bg-background a-hover:bg-accent a-hover:text-accent-foreground",
+ secondary: "a-bg-secondary a-text-secondary-foreground a-hover:bg-secondary/80",
+ ghost: "a-hover:bg-accent a-hover:text-accent-foreground",
+ link: "a-text-primary a-underline-offset-4 a-hover:underline",
+ },
+ size: {
+ default: "a-h-10 a-px-4 a-py-2",
+ sm: "a-h-9 a-rounded-md a-px-3",
+ lg: "a-h-11 a-rounded-md a-px-8",
+ icon: "a-h-10 a-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+});
+
+export {
+ Root,
+ //
+ Root as Button,
+ buttonVariants,
+};
diff --git a/src/test/testData/classReplacement/fullSvelteVue_after.ts b/src/test/testData/classReplacement/fullSvelteVue_after.ts
new file mode 100644
index 0000000..4d85644
--- /dev/null
+++ b/src/test/testData/classReplacement/fullSvelteVue_after.ts
@@ -0,0 +1,49 @@
+import { tv, type VariantProps } from "tailwind-variants";
+import type { Button as ButtonPrimitive } from "bits-ui";
+import Root from "./button.svelte";
+
+const buttonVariants = tv({
+ base: "a-inline-flex a-items-center a-justify-center a-whitespace-nowrap a-rounded-md a-text-sm a-font-medium a-ring-offset-background a-transition-colors a-focus-visible:outline-none a-focus-visible:ring-2 a-focus-visible:ring-ring a-focus-visible:ring-offset-2 a-disabled:pointer-events-none a-disabled:opacity-50",
+ variants: {
+ variant: {
+ default: "a-bg-primary a-text-primary-foreground a-hover:bg-primary/90",
+ destructive: "a-bg-destructive a-text-destructive-foreground a-hover:bg-destructive/90",
+ outline:
+ "a-border a-border-input a-bg-background a-hover:bg-accent a-hover:text-accent-foreground",
+ secondary: "a-bg-secondary a-text-secondary-foreground a-hover:bg-secondary/80",
+ ghost: "a-hover:bg-accent a-hover:text-accent-foreground",
+ link: "a-text-primary a-underline-offset-4 a-hover:underline",
+ },
+ size: {
+ default: "a-h-10 a-px-4 a-py-2",
+ sm: "a-h-9 a-rounded-md a-px-3",
+ lg: "a-h-11 a-rounded-md a-px-8",
+ icon: "a-h-10 a-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+});
+
+type Variant = VariantProps["variant"];
+type Size = VariantProps["size"];
+
+type Props = ButtonPrimitive.Props & {
+ variant?: Variant;
+ size?: Size;
+};
+
+type Events = ButtonPrimitive.Events;
+
+export {
+ Root,
+ type Props,
+ type Events,
+ //
+ Root as Button,
+ type Props as ButtonProps,
+ type Events as ButtonEvents,
+ buttonVariants,
+};
diff --git a/src/test/testData/classReplacement/nestedAndOtherAttributes.jsx b/src/test/testData/classReplacement/nestedAndOtherAttributes.jsx
new file mode 100644
index 0000000..51697ef
--- /dev/null
+++ b/src/test/testData/classReplacement/nestedAndOtherAttributes.jsx
@@ -0,0 +1,4 @@
+const tag =
+
Hello, world!
+
Goodbye, world!
+
diff --git a/src/test/testData/classReplacement/nestedAndOtherAttributes_after.jsx b/src/test/testData/classReplacement/nestedAndOtherAttributes_after.jsx
new file mode 100644
index 0000000..81dc4a6
--- /dev/null
+++ b/src/test/testData/classReplacement/nestedAndOtherAttributes_after.jsx
@@ -0,0 +1,4 @@
+const tag =
+
Hello, world!
+
Goodbye, world!
+
diff --git a/src/test/testData/classReplacement/simpleOtherAttribute.jsx b/src/test/testData/classReplacement/simpleOtherAttribute.jsx
new file mode 100644
index 0000000..6af6f98
--- /dev/null
+++ b/src/test/testData/classReplacement/simpleOtherAttribute.jsx
@@ -0,0 +1 @@
+Hello, world!
;
diff --git a/src/test/testData/classReplacement/simpleOtherAttribute_after.jsx b/src/test/testData/classReplacement/simpleOtherAttribute_after.jsx
new file mode 100644
index 0000000..6af6f98
--- /dev/null
+++ b/src/test/testData/classReplacement/simpleOtherAttribute_after.jsx
@@ -0,0 +1 @@
+Hello, world!
;
diff --git a/src/test/testData/classReplacement/singleQuotes.tsx b/src/test/testData/classReplacement/singleQuotes.tsx
new file mode 100644
index 0000000..a0f2b0f
--- /dev/null
+++ b/src/test/testData/classReplacement/singleQuotes.tsx
@@ -0,0 +1 @@
+const tag = Hello, world!
;
diff --git a/src/test/testData/classReplacement/singleQuotes_after.tsx b/src/test/testData/classReplacement/singleQuotes_after.tsx
new file mode 100644
index 0000000..5d8ba51
--- /dev/null
+++ b/src/test/testData/classReplacement/singleQuotes_after.tsx
@@ -0,0 +1 @@
+const tag = Hello, world!
;
diff --git a/src/test/testData/importsReplacement/imports.js b/src/test/testData/importsReplacement/imports.js
new file mode 100644
index 0000000..41a2983
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports.js
@@ -0,0 +1,5 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/registry/default/ui/button"
diff --git a/src/test/testData/importsReplacement/imports.jsx b/src/test/testData/importsReplacement/imports.jsx
new file mode 100644
index 0000000..41a2983
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports.jsx
@@ -0,0 +1,5 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/registry/default/ui/button"
diff --git a/src/test/testData/importsReplacement/imports.ts b/src/test/testData/importsReplacement/imports.ts
new file mode 100644
index 0000000..aec67c3
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports.ts
@@ -0,0 +1,5 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
diff --git a/src/test/testData/importsReplacement/imports.tsx b/src/test/testData/importsReplacement/imports.tsx
new file mode 100644
index 0000000..aec67c3
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports.tsx
@@ -0,0 +1,5 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
diff --git a/src/test/testData/importsReplacement/importsJS.svelte b/src/test/testData/importsReplacement/importsJS.svelte
new file mode 100644
index 0000000..ee8aa74
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsJS.svelte
@@ -0,0 +1,5 @@
+
diff --git a/src/test/testData/importsReplacement/importsJS.vue b/src/test/testData/importsReplacement/importsJS.vue
new file mode 100644
index 0000000..e9a8a35
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsJS.vue
@@ -0,0 +1,5 @@
+
diff --git a/src/test/testData/importsReplacement/importsJS_after.svelte b/src/test/testData/importsReplacement/importsJS_after.svelte
new file mode 100644
index 0000000..23b79c8
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsJS_after.svelte
@@ -0,0 +1,5 @@
+
diff --git a/src/test/testData/importsReplacement/importsJS_after.vue b/src/test/testData/importsReplacement/importsJS_after.vue
new file mode 100644
index 0000000..d3f16ec
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsJS_after.vue
@@ -0,0 +1,5 @@
+
diff --git a/src/test/testData/importsReplacement/importsTS.svelte b/src/test/testData/importsReplacement/importsTS.svelte
new file mode 100644
index 0000000..39d5253
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsTS.svelte
@@ -0,0 +1,5 @@
+
diff --git a/src/test/testData/importsReplacement/importsTS.vue b/src/test/testData/importsReplacement/importsTS.vue
new file mode 100644
index 0000000..ed256bc
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsTS.vue
@@ -0,0 +1,12 @@
+
diff --git a/src/test/testData/importsReplacement/importsTS_after.svelte b/src/test/testData/importsReplacement/importsTS_after.svelte
new file mode 100644
index 0000000..f4c83bd
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsTS_after.svelte
@@ -0,0 +1,5 @@
+
diff --git a/src/test/testData/importsReplacement/importsTS_after.vue b/src/test/testData/importsReplacement/importsTS_after.vue
new file mode 100644
index 0000000..53d9704
--- /dev/null
+++ b/src/test/testData/importsReplacement/importsTS_after.vue
@@ -0,0 +1,12 @@
+
diff --git a/src/test/testData/importsReplacement/imports_after.js b/src/test/testData/importsReplacement/imports_after.js
new file mode 100644
index 0000000..e6ee7c8
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports_after.js
@@ -0,0 +1,5 @@
+import * as React from "a-react"
+import * as AlertDialogPrimitive from "a-@radix-ui/react-alert-dialog"
+
+import { cn } from "a-@/lib/utils"
+import { buttonVariants } from "a-@/registry/default/ui/button"
diff --git a/src/test/testData/importsReplacement/imports_after.jsx b/src/test/testData/importsReplacement/imports_after.jsx
new file mode 100644
index 0000000..e6ee7c8
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports_after.jsx
@@ -0,0 +1,5 @@
+import * as React from "a-react"
+import * as AlertDialogPrimitive from "a-@radix-ui/react-alert-dialog"
+
+import { cn } from "a-@/lib/utils"
+import { buttonVariants } from "a-@/registry/default/ui/button"
diff --git a/src/test/testData/importsReplacement/imports_after.ts b/src/test/testData/importsReplacement/imports_after.ts
new file mode 100644
index 0000000..9f43b11
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports_after.ts
@@ -0,0 +1,5 @@
+import * as React from "a-react"
+import { Slot } from "a-@radix-ui/react-slot"
+import { cva, type VariantProps } from "a-class-variance-authority"
+
+import { cn } from "a-@/lib/utils"
diff --git a/src/test/testData/importsReplacement/imports_after.tsx b/src/test/testData/importsReplacement/imports_after.tsx
new file mode 100644
index 0000000..9f43b11
--- /dev/null
+++ b/src/test/testData/importsReplacement/imports_after.tsx
@@ -0,0 +1,5 @@
+import * as React from "a-react"
+import { Slot } from "a-@radix-ui/react-slot"
+import { cva, type VariantProps } from "a-class-variance-authority"
+
+import { cn } from "a-@/lib/utils"
diff --git a/src/test/testData/reactDirectiveRemoval/react.jsx b/src/test/testData/reactDirectiveRemoval/react.jsx
new file mode 100644
index 0000000..78e2aae
--- /dev/null
+++ b/src/test/testData/reactDirectiveRemoval/react.jsx
@@ -0,0 +1,10 @@
+"use client";
+
+import { useState } from "react";
+
+export default function ReactDirectiveRemoval() {
+ const [state, _] = useState(0);
+ return {
+ state === 0 ?
0
:
1
+ }
;
+}
diff --git a/src/test/testData/reactDirectiveRemoval/react.tsx b/src/test/testData/reactDirectiveRemoval/react.tsx
new file mode 100644
index 0000000..e2a2531
--- /dev/null
+++ b/src/test/testData/reactDirectiveRemoval/react.tsx
@@ -0,0 +1,10 @@
+'use server'
+
+import { useState } from "react";
+
+export default function ReactDirectiveRemoval() {
+ const [state, _] = useState(0);
+ return {
+ state === 0 ?
0
:
1
+ }
;
+}
diff --git a/src/test/testData/reactDirectiveRemoval/react_after.jsx b/src/test/testData/reactDirectiveRemoval/react_after.jsx
new file mode 100644
index 0000000..0aae0e3
--- /dev/null
+++ b/src/test/testData/reactDirectiveRemoval/react_after.jsx
@@ -0,0 +1,10 @@
+
+
+import { useState } from "react";
+
+export default function ReactDirectiveRemoval() {
+ const [state, _] = useState(0);
+ return {
+ state === 0 ?
0
:
1
+ }
;
+}
diff --git a/src/test/testData/reactDirectiveRemoval/react_after.tsx b/src/test/testData/reactDirectiveRemoval/react_after.tsx
new file mode 100644
index 0000000..0aae0e3
--- /dev/null
+++ b/src/test/testData/reactDirectiveRemoval/react_after.tsx
@@ -0,0 +1,10 @@
+
+
+import { useState } from "react";
+
+export default function ReactDirectiveRemoval() {
+ const [state, _] = useState(0);
+ return {
+ state === 0 ?
0
:
1
+ }
;
+}