Skip to content

Commit

Permalink
feat(cli): add support for custom Tailwind prefix transformer (#770)
Browse files Browse the repository at this point in the history
* feat(cli): add support for custom Tailwind prefix

* fix(cli): add tw prefix on classes applied in the css file

* feat(cli): add support for custom tailwind prefix

* chore: add changeset

* style(shadcn-ui): code format

---------

Co-authored-by: shadcn <[email protected]>
  • Loading branch information
Bekacru and shadcn committed Dec 21, 2023
1 parent 1cf5fad commit 4fb98d5
Show file tree
Hide file tree
Showing 11 changed files with 477 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-mugs-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn-ui": minor
---

add support for custom tailwind prefix
20 changes: 18 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import ora from "ora"
import prompts from "prompts"
import * as z from "zod"

import { applyPrefixesCss } from "../utils/transformers/transform-tw-prefix"

const PROJECT_DEPENDENCIES = [
"tailwindcss-animate",
"class-variance-authority",
Expand Down Expand Up @@ -132,6 +134,14 @@ export async function promptForConfig(
active: "yes",
inactive: "no",
},
{
type: "text",
name: "tailwindPrefix",
message: `Are you using a custom ${highlight(
"tailwind prefix eg. tw-"
)}? (Leave blank if not)`,
initial: "",
},
{
type: "text",
name: "tailwindConfig",
Expand Down Expand Up @@ -168,6 +178,7 @@ export async function promptForConfig(
css: options.tailwindCss,
baseColor: options.tailwindBaseColor,
cssVariables: options.tailwindCssVariables,
prefix: options.tailwindPrefix,
},
rsc: options.rsc,
tsx: options.typescript,
Expand Down Expand Up @@ -246,7 +257,10 @@ export async function runInit(cwd: string, config: Config) {
// Write tailwind config.
await fs.writeFile(
config.resolvedPaths.tailwindConfig,
template(tailwindConfigTemplate)({ extension }),
template(tailwindConfigTemplate)({
extension,
prefix: config.tailwind.prefix,
}),
"utf8"
)

Expand All @@ -256,7 +270,9 @@ export async function runInit(cwd: string, config: Config) {
await fs.writeFile(
config.resolvedPaths.tailwindCss,
config.tailwind.cssVariables
? baseColor.cssVarsTemplate
? config.tailwind.prefix
? applyPrefixesCss(baseColor.cssVarsTemplate, config.tailwind.prefix)
: baseColor.cssVarsTemplate
: baseColor.inlineColorsTemplate,
"utf8"
)
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/utils/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const rawConfigSchema = z
css: z.string(),
baseColor: z.string(),
cssVariables: z.boolean().default(true),
prefix: z.string().default("").optional(),
}),
aliases: z.object({
components: z.string(),
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/utils/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
Expand Down Expand Up @@ -60,6 +61,7 @@ module.exports = {
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
Expand Down Expand Up @@ -138,6 +140,7 @@ const config = {
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
Expand Down Expand Up @@ -178,6 +181,7 @@ const config = {
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/utils/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { Project, ScriptKind, type SourceFile } from "ts-morph"
import * as z from "zod"

import { transformTwPrefixes } from "./transform-tw-prefix"

export type TransformOpts = {
filename: string
raw: string
Expand All @@ -27,6 +29,7 @@ const transformers: Transformer[] = [
transformImport,
transformRsc,
transformCssVars,
transformTwPrefixes,
]

const project = new Project({
Expand Down
201 changes: 201 additions & 0 deletions packages/cli/src/utils/transformers/transform-tw-prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { Transformer } from "@/src/utils/transformers"
import { SyntaxKind } from "ts-morph"

import { splitClassName } from "./transform-css-vars"

export const transformTwPrefixes: Transformer = async ({
sourceFile,
config,
}) => {
if (!config.tailwind?.prefix) {
return sourceFile
}

// Find the cva function calls.
sourceFile
.getDescendantsOfKind(SyntaxKind.CallExpression)
.filter((node) => node.getExpression().getText() === "cva")
.forEach((node) => {
// cva(base, ...)
if (node.getArguments()[0]?.isKind(SyntaxKind.StringLiteral)) {
const defaultClassNames = node.getArguments()[0]
if (defaultClassNames) {
defaultClassNames.replaceWithText(
`"${applyPrefix(
defaultClassNames.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
}
}

// cva(..., { variants: { ... } })
if (node.getArguments()[1]?.isKind(SyntaxKind.ObjectLiteralExpression)) {
node
.getArguments()[1]
?.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
.find((node) => node.getName() === "variants")
?.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
.forEach((node) => {
node
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
.forEach((node) => {
const classNames = node.getInitializerIfKind(
SyntaxKind.StringLiteral
)
if (classNames) {
classNames?.replaceWithText(
`"${applyPrefix(
classNames.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
}
})
})
}
})

// Find all jsx attributes with the name className.
sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((node) => {
if (node.getName() === "className") {
// className="..."
if (node.getInitializer()?.isKind(SyntaxKind.StringLiteral)) {
const value = node.getInitializer()
if (value) {
value.replaceWithText(
`"${applyPrefix(
value.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
}
}

// className={...}
if (node.getInitializer()?.isKind(SyntaxKind.JsxExpression)) {
// Check if it's a call to cn().
const callExpression = node
.getInitializer()
?.getDescendantsOfKind(SyntaxKind.CallExpression)
.find((node) => node.getExpression().getText() === "cn")
if (callExpression) {
// Loop through the arguments.
callExpression.getArguments().forEach((node) => {
if (
node.isKind(SyntaxKind.ConditionalExpression) ||
node.isKind(SyntaxKind.BinaryExpression)
) {
node
.getChildrenOfKind(SyntaxKind.StringLiteral)
.forEach((node) => {
node.replaceWithText(
`"${applyPrefix(
node.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
})
}

if (node.isKind(SyntaxKind.StringLiteral)) {
node.replaceWithText(
`"${applyPrefix(
node.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
}
})
}
}
}

// classNames={...}
if (node.getName() === "classNames") {
if (node.getInitializer()?.isKind(SyntaxKind.JsxExpression)) {
node
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
.forEach((node) => {
if (node.getInitializer()?.isKind(SyntaxKind.CallExpression)) {
const callExpression = node.getInitializerIfKind(
SyntaxKind.CallExpression
)
if (callExpression) {
// Loop through the arguments.
callExpression.getArguments().forEach((arg) => {
if (arg.isKind(SyntaxKind.ConditionalExpression)) {
arg
.getChildrenOfKind(SyntaxKind.StringLiteral)
.forEach((node) => {
node.replaceWithText(
`"${applyPrefix(
node.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
})
}

if (arg.isKind(SyntaxKind.StringLiteral)) {
arg.replaceWithText(
`"${applyPrefix(
arg.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
}
})
}
}

if (node.getInitializer()?.isKind(SyntaxKind.StringLiteral)) {
if (node.getName() !== "variant") {
const classNames = node.getInitializer()
if (classNames) {
classNames.replaceWithText(
`"${applyPrefix(
classNames.getText()?.replace(/"/g, ""),
config.tailwind.prefix
)}"`
)
}
}
}
})
}
}
})

return sourceFile
}

export function applyPrefix(input: string, prefix: string = "") {
const classNames = input.split(" ")
const prefixed: string[] = []
for (let className of classNames) {
const [variant, value, modifier] = splitClassName(className)
if (variant) {
modifier
? prefixed.push(`${variant}:${prefix}${value}/${modifier}`)
: prefixed.push(`${variant}:${prefix}${value}`)
} else {
modifier
? prefixed.push(`${prefix}${value}/${modifier}`)
: prefixed.push(`${prefix}${value}`)
}
}
return prefixed.join(" ")
}

export function applyPrefixesCss(css: string, prefix: string) {
const lines = css.split("\n")
for (let line of lines) {
if (line.includes("@apply")) {
const originalTWCls = line.replace("@apply", "").trim()
const prefixedTwCls = applyPrefix(originalTWCls, prefix)
css = css.replace(originalTWCls, prefixedTwCls)
}
}
return css
}
3 changes: 2 additions & 1 deletion packages/cli/test/fixtures/config-full/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true
"cssVariables": true,
"prefix": "tw-"
},
"rsc": false,
"aliases": {
Expand Down
Loading

0 comments on commit 4fb98d5

Please sign in to comment.