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 @@ + + + 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 @@ + + + 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
+ }
; +}