diff --git a/.changeset/plenty-mugs-applaud.md b/.changeset/plenty-mugs-applaud.md new file mode 100644 index 0000000000..6a8204e599 --- /dev/null +++ b/.changeset/plenty-mugs-applaud.md @@ -0,0 +1,5 @@ +--- +"shadcn-ui": minor +--- + +add support for custom tailwind prefix diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 2653af78c9..1fe39a5b10 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -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", @@ -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", @@ -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, @@ -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" ) @@ -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" ) diff --git a/packages/cli/src/utils/get-config.ts b/packages/cli/src/utils/get-config.ts index 4ea29431c2..3aac937804 100644 --- a/packages/cli/src/utils/get-config.ts +++ b/packages/cli/src/utils/get-config.ts @@ -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(), diff --git a/packages/cli/src/utils/templates.ts b/packages/cli/src/utils/templates.ts index f110cf90fc..f602416b4d 100644 --- a/packages/cli/src/utils/templates.ts +++ b/packages/cli/src/utils/templates.ts @@ -23,6 +23,7 @@ module.exports = { './app/**/*.{<%- extension %>,<%- extension %>x}', './src/**/*.{<%- extension %>,<%- extension %>x}', ], + prefix: "<%- prefix %>", theme: { container: { center: true, @@ -60,6 +61,7 @@ module.exports = { './app/**/*.{<%- extension %>,<%- extension %>x}', './src/**/*.{<%- extension %>,<%- extension %>x}', ], + prefix: "<%- prefix %>", theme: { container: { center: true, @@ -138,6 +140,7 @@ const config = { './app/**/*.{<%- extension %>,<%- extension %>x}', './src/**/*.{<%- extension %>,<%- extension %>x}', ], + prefix: "<%- prefix %>", theme: { container: { center: true, @@ -178,6 +181,7 @@ const config = { './app/**/*.{<%- extension %>,<%- extension %>x}', './src/**/*.{<%- extension %>,<%- extension %>x}', ], + prefix: "<%- prefix %>", theme: { container: { center: true, diff --git a/packages/cli/src/utils/transformers/index.ts b/packages/cli/src/utils/transformers/index.ts index b5699d71e6..daa1bb73bc 100644 --- a/packages/cli/src/utils/transformers/index.ts +++ b/packages/cli/src/utils/transformers/index.ts @@ -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 @@ -27,6 +29,7 @@ const transformers: Transformer[] = [ transformImport, transformRsc, transformCssVars, + transformTwPrefixes, ] const project = new Project({ diff --git a/packages/cli/src/utils/transformers/transform-tw-prefix.ts b/packages/cli/src/utils/transformers/transform-tw-prefix.ts new file mode 100644 index 0000000000..1fed9a883f --- /dev/null +++ b/packages/cli/src/utils/transformers/transform-tw-prefix.ts @@ -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 +} diff --git a/packages/cli/test/fixtures/config-full/components.json b/packages/cli/test/fixtures/config-full/components.json index 3039e5a6e7..675abb22e9 100644 --- a/packages/cli/test/fixtures/config-full/components.json +++ b/packages/cli/test/fixtures/config-full/components.json @@ -4,7 +4,8 @@ "config": "tailwind.config.ts", "css": "src/app/globals.css", "baseColor": "zinc", - "cssVariables": true + "cssVariables": true, + "prefix": "tw-" }, "rsc": false, "aliases": { diff --git a/packages/cli/test/utils/__snapshots__/transform-tw-prefix.test.ts.snap b/packages/cli/test/utils/__snapshots__/transform-tw-prefix.test.ts.snap new file mode 100644 index 0000000000..7667c37104 --- /dev/null +++ b/packages/cli/test/utils/__snapshots__/transform-tw-prefix.test.ts.snap @@ -0,0 +1,118 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transform tailwind prefix 1`] = ` +"import * as React from \\"react\\" + export function Foo() { + return ( +