diff --git a/packages/shadcn/src/utils/add-components.ts b/packages/shadcn/src/utils/add-components.ts index 8d5a8b6e07d..c7aba2217a2 100644 --- a/packages/shadcn/src/utils/add-components.ts +++ b/packages/shadcn/src/utils/add-components.ts @@ -85,6 +85,7 @@ async function addProjectComponents( cleanupDefaultNextStyles: options.isNewProject, silent: options.silent, tailwindVersion, + tailwindConfig: tree.tailwind?.config, }) await updateDependencies(tree.dependencies, config, { @@ -176,6 +177,7 @@ async function addWorkspaceComponents( await updateCssVars(component.cssVars, targetConfig, { silent: true, tailwindVersion, + tailwindConfig: component.tailwind?.config, }) filesUpdated.push( path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindCss) diff --git a/packages/shadcn/src/utils/updaters/update-css-vars.ts b/packages/shadcn/src/utils/updaters/update-css-vars.ts index 8d4cb88753c..59716ae786e 100644 --- a/packages/shadcn/src/utils/updaters/update-css-vars.ts +++ b/packages/shadcn/src/utils/updaters/update-css-vars.ts @@ -1,6 +1,9 @@ import { promises as fs } from "fs" import path from "path" -import { registryItemCssVarsSchema } from "@/src/registry/schema" +import { + registryItemCssVarsSchema, + registryItemTailwindSchema, +} from "@/src/registry/schema" import { Config } from "@/src/utils/get-config" import { TailwindVersion, getProjectInfo } from "@/src/utils/get-project-info" import { highlighter } from "@/src/utils/highlighter" @@ -18,13 +21,10 @@ export async function updateCssVars( cleanupDefaultNextStyles?: boolean silent?: boolean tailwindVersion?: TailwindVersion + tailwindConfig?: z.infer["config"] } ) { - if ( - !cssVars || - !Object.keys(cssVars).length || - !config.resolvedPaths.tailwindCss - ) { + if (!config.resolvedPaths.tailwindCss) { return } @@ -46,9 +46,10 @@ export async function updateCssVars( } ).start() const raw = await fs.readFile(cssFilepath, "utf8") - let output = await transformCssVars(raw, cssVars, config, { + let output = await transformCssVars(raw, cssVars ?? {}, config, { cleanupDefaultNextStyles: options.cleanupDefaultNextStyles, tailwindVersion: options.tailwindVersion, + tailwindConfig: options.tailwindConfig, }) await fs.writeFile(cssFilepath, output, "utf8") cssVarsSpinner.succeed() @@ -61,14 +62,17 @@ export async function transformCssVars( options: { cleanupDefaultNextStyles?: boolean tailwindVersion?: TailwindVersion + tailwindConfig?: z.infer["config"] } = { cleanupDefaultNextStyles: false, tailwindVersion: "v3", + tailwindConfig: undefined, } ) { options = { cleanupDefaultNextStyles: false, tailwindVersion: "v3", + tailwindConfig: undefined, ...options, } @@ -80,6 +84,10 @@ export async function transformCssVars( updateCssVarsPluginV4(cssVars), updateThemePlugin(cssVars), ] + + if (options.tailwindConfig) { + plugins.push(updateTailwindConfigPlugin(options.tailwindConfig)) + } } if (options.cleanupDefaultNextStyles) { @@ -371,6 +379,19 @@ function updateThemePlugin(cssVars: z.infer) { return { postcssPlugin: "update-theme", Once(root: Root) { + // Find unique color names from light and dark. + const colors = Array.from( + new Set( + Object.keys(cssVars).flatMap((key) => + Object.keys(cssVars[key as keyof typeof cssVars] || {}) + ) + ) + ) + + if (!colors.length) { + return + } + let themeNode = root.nodes.find( (node): node is AtRule => node.type === "atrule" && @@ -388,15 +409,6 @@ function updateThemePlugin(cssVars: z.infer) { root.append(themeNode) } - // Find unique color names from light and dark. - const colors = Array.from( - new Set( - Object.keys(cssVars).flatMap((key) => - Object.keys(cssVars[key as keyof typeof cssVars] || {}) - ) - ) - ) - for (const color of colors) { const colorVar = postcss.decl({ prop: `--color-${color.replace(/^--/, "")}`, @@ -436,3 +448,59 @@ function addCustomVariant({ params }: { params: string }) { }, } } + +function updateTailwindConfigPlugin( + tailwindConfig: z.infer["config"] +) { + return { + postcssPlugin: "update-tailwind-config", + Once(root: Root) { + if (!tailwindConfig?.plugins) { + return + } + + const quoteType = getQuoteType(root) + const quote = quoteType === "single" ? "'" : '"' + + const pluginNodes = root.nodes.filter( + (node): node is AtRule => + node.type === "atrule" && node.name === "plugin" + ) + + const lastPluginNode = + pluginNodes[pluginNodes.length - 1] || root.nodes[0] + + for (const plugin of tailwindConfig.plugins) { + const pluginName = plugin.replace(/^require\(["']|["']\)$/g, "") + + // Check if the plugin is already present. + if ( + pluginNodes.some((node) => { + return node.params.replace(/["']/g, "") === pluginName + }) + ) { + continue + } + + root.insertAfter( + lastPluginNode, + postcss.atRule({ + name: "plugin", + params: `${quote}${pluginName}${quote}`, + raws: { semicolon: true, before: "\n" }, + }) + ) + } + }, + } +} + +function getQuoteType(root: Root): "single" | "double" { + const firstNode = root.nodes[0] + const raw = firstNode.toString() + + if (raw.includes("'")) { + return "single" + } + return "double" +} diff --git a/packages/shadcn/test/utils/updaters/update-css-vars.test.ts b/packages/shadcn/test/utils/updaters/update-css-vars.test.ts index 641d8e67b1e..db783657fd3 100644 --- a/packages/shadcn/test/utils/updaters/update-css-vars.test.ts +++ b/packages/shadcn/test/utils/updaters/update-css-vars.test.ts @@ -548,4 +548,101 @@ describe("transformCssVarsV4", () => { " `) }) + + test("should add plugin if not present", async () => { + expect( + await transformCssVars( + `@import "tailwindcss"; + `, + {}, + { tailwind: { cssVariables: true } }, + { + tailwindVersion: "v4", + tailwindConfig: { plugins: ['require("tailwindcss-animate")'] }, + } + ) + ).toMatchInlineSnapshot(` + "@import "tailwindcss"; + @plugin "tailwindcss-animate"; + @custom-variant dark (&:is(.dark *)); + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } + " + `) + }) + + test("should NOT add plugin if already present", async () => { + expect( + await transformCssVars( + `@import "tailwindcss"; + @plugin "tailwindcss-animate"; + `, + {}, + { tailwind: { cssVariables: true } }, + { + tailwindVersion: "v4", + tailwindConfig: { + plugins: [ + 'require("tailwindcss-animate")', + 'require("@tailwindcss/typography")', + ], + }, + } + ) + ).toMatchInlineSnapshot(` + "@import "tailwindcss"; + @custom-variant dark (&:is(.dark *)); + @plugin "tailwindcss-animate"; + @plugin "@tailwindcss/typography"; + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } + " + `) + }) + + test("should preserve quotes", async () => { + expect( + await transformCssVars( + `@import 'tailwindcss'; + `, + {}, + { tailwind: { cssVariables: true } }, + { + tailwindVersion: "v4", + tailwindConfig: { + plugins: [ + 'require("tailwindcss-animate")', + 'require("@tailwindcss/typography")', + ], + }, + } + ) + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + @plugin '@tailwindcss/typography'; + @plugin 'tailwindcss-animate'; + @custom-variant dark (&:is(.dark *)); + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } + " + `) + }) })