diff --git a/packages/cli/src/utils/updaters/update-tailwind-config.ts b/packages/cli/src/utils/updaters/update-tailwind-config.ts index 289a37b260..8510deff59 100644 --- a/packages/cli/src/utils/updaters/update-tailwind-config.ts +++ b/packages/cli/src/utils/updaters/update-tailwind-config.ts @@ -25,20 +25,14 @@ export async function updateTailwindConfig( config: Config ) { const raw = await fs.readFile(config.resolvedPaths.tailwindConfig, "utf8") - const output = await transformTailwindConfig(raw, tailwindConfig, { - config, - }) + const output = await transformTailwindConfig(raw, tailwindConfig, config) await fs.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8") } export async function transformTailwindConfig( input: string, tailwindConfig: UpdaterTailwindConfig, - { - config, - }: { - config: Config - } + config: Config ) { const sourceFile = await _createSourceFile(input, config) // Find the object with content property. @@ -231,7 +225,7 @@ function addTailwindConfigPlugin( async function _createSourceFile(input: string, config: Config | null) { const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-")) const resolvedPath = - config?.resolvedPaths.tailwindConfig || "tailwind.config.ts" + config?.resolvedPaths?.tailwindConfig || "tailwind.config.ts" const tempFile = path.join(dir, `shadcn-${path.basename(resolvedPath)}`) const project = new Project({ diff --git a/packages/cli/src/utils/updaters/update-tailwind-css.ts b/packages/cli/src/utils/updaters/update-tailwind-css.ts index a9da886a56..125959ad4f 100644 --- a/packages/cli/src/utils/updaters/update-tailwind-css.ts +++ b/packages/cli/src/utils/updaters/update-tailwind-css.ts @@ -3,7 +3,6 @@ import { Config } from "@/src/utils/get-config" import { registryCssVarsSchema } from "@/src/utils/registry/schema" import postcss from "postcss" import AtRule from "postcss/lib/at-rule" -import Node from "postcss/lib/node" import Root from "postcss/lib/root" import Rule from "postcss/lib/rule" import { z } from "zod" @@ -13,7 +12,8 @@ export async function updateTailwindCss( config: Config ) { const raw = await fs.readFile(config.resolvedPaths.tailwindCss, "utf8") - const output = await transformTailwindCss(raw, cssVars) + let output = await transformTailwindCss(raw, cssVars) + await fs.writeFile(config.resolvedPaths.tailwindCss, output, "utf8") } @@ -21,107 +21,137 @@ export async function transformTailwindCss( input: string, cssVars: z.infer ) { - const insertCssVarsPlugin = () => { - return { - postcssPlugin: "insert-css-vars", - Once(root: Root) { - let baseLayer = root.nodes.find( - (node) => - node.type === "atrule" && - node.name === "layer" && - node.params === "base" - ) as AtRule | undefined - - if (!(baseLayer instanceof AtRule)) { - baseLayer = postcss.atRule({ - name: "layer", - params: "base", - nodes: [], - raws: { semicolon: true }, - }) - root.append(baseLayer) - } - - // First pass: Add or update variables - if (cssVars.light) { - let lightVars = baseLayer.nodes?.find( - (node: Node) => node instanceof Rule && node.selector === ":root" - ) as Rule | undefined - - if (!lightVars) { - lightVars = postcss.rule({ selector: ":root" }) - baseLayer.append(lightVars) - } - - Object.entries(cssVars.light).forEach(([key, value]) => { - const existingDecl = lightVars.nodes.find( - (node) => node.type === "decl" && node.prop === `--${key}` - ) - if (existingDecl) { - existingDecl.replaceWith( - postcss.decl({ - prop: `--${key}`, - value, - raws: { semicolon: true }, - }) - ) - } else { - lightVars.append({ - prop: `--${key}`, - value, - raws: { semicolon: true }, - }) - } - }) - } - - if (cssVars.dark) { - let darkVars = baseLayer.nodes?.find( - (node: Node) => node instanceof Rule && node.selector === ".dark" - ) as Rule | undefined - - if (!darkVars) { - darkVars = postcss.rule({ selector: ".dark" }) - baseLayer.append(darkVars) - } - - Object.entries(cssVars.dark).forEach(([key, value]) => { - const existingDecl = darkVars.nodes.find( - (node) => node.type === "decl" && node.prop === `--${key}` - ) - if (existingDecl) { - existingDecl.replaceWith( - postcss.decl({ - prop: `--${key}`, - value, - raws: { semicolon: true }, - }) - ) - } else { - darkVars.append({ - prop: `--${key}`, - value, - raws: { semicolon: true }, - }) - } - }) - } - - // Second pass: Add missing semicolons - // baseLayer.walkRules((rule) => { - // if (rule.selector === ":root" || rule.selector === ".dark") { - // rule.walkDecls((decl) => { - // decl.value = decl.value.replace(/;$/, "") + ";" - // }) - // } - // }) - }, - } - } - - const result = await postcss([insertCssVarsPlugin()]).process(input, { + const result = await postcss([ + updateCssVarsPlugin(cssVars), + updateBaseLayerPlugin(), + ]).process(input, { from: undefined, }) return result.css } + +function updateBaseLayerPlugin() { + return { + postcssPlugin: "update-base-layer", + Once(root: Root) { + const requiredRules = [ + { selector: "*", apply: "border-border" }, + { selector: "body", apply: "bg-background text-foreground" }, + ] + + let baseLayer = root.nodes.find( + (node): node is AtRule => + node.type === "atrule" && + node.name === "layer" && + node.params === "base" && + requiredRules.every(({ selector, apply }) => + node.nodes?.some( + (rule): rule is Rule => + rule.type === "rule" && + rule.selector === selector && + rule.nodes.some( + (applyRule): applyRule is AtRule => + applyRule.type === "atrule" && + applyRule.name === "apply" && + applyRule.params === apply + ) + ) + ) + ) as AtRule | undefined + + if (!baseLayer) { + baseLayer = postcss.atRule({ + name: "layer", + params: "base", + raws: { semicolon: true, between: " " }, + }) + root.append(baseLayer) + } + + requiredRules.forEach(({ selector, apply }) => { + const existingRule = baseLayer.nodes?.find( + (node): node is Rule => + node.type === "rule" && node.selector === selector + ) + + if (!existingRule) { + baseLayer.append( + postcss.rule({ + selector, + nodes: [ + postcss.atRule({ + name: "apply", + params: apply, + }), + ], + raws: { semicolon: true, between: " " }, + }) + ) + } + }) + }, + } +} + +function updateCssVarsPlugin(cssVars: z.infer) { + return { + postcssPlugin: "update-css-vars", + Once(root: Root) { + let baseLayer = root.nodes.find( + (node) => + node.type === "atrule" && + node.name === "layer" && + node.params === "base" + ) as AtRule | undefined + + if (!(baseLayer instanceof AtRule)) { + baseLayer = postcss.atRule({ + name: "layer", + params: "base", + nodes: [], + raws: { semicolon: true }, + }) + root.append(baseLayer) + } + + // Add variables for each key in cssVars + Object.entries(cssVars).forEach(([key, vars]) => { + const selector = key === "light" ? ":root" : `.${key}` + addOrUpdateVars(baseLayer, selector, vars) + }) + }, + } +} + +// Function to add or update variables for a given selector +function addOrUpdateVars( + baseLayer: AtRule, + selector: string, + vars: Record +) { + let ruleNode = baseLayer.nodes?.find( + (node): node is Rule => node.type === "rule" && node.selector === selector + ) + + if (!ruleNode) { + ruleNode = postcss.rule({ selector }) + baseLayer.append(ruleNode) + } + + Object.entries(vars).forEach(([key, value]) => { + const prop = `--${key}` + const newDecl = postcss.decl({ + prop, + value, + raws: { semicolon: true }, + }) + + const existingDecl = ruleNode.nodes.find( + (node): node is postcss.Declaration => + node.type === "decl" && node.prop === prop + ) + + existingDecl ? existingDecl.replaceWith(newDecl) : ruleNode.append(newDecl) + }) +} diff --git a/packages/cli/test/commands/init.test.ts b/packages/cli/test/commands/init.test.ts index a8d60b4e06..7b6e4fb421 100644 --- a/packages/cli/test/commands/init.test.ts +++ b/packages/cli/test/commands/init.test.ts @@ -70,16 +70,11 @@ test("init config-full", async () => { expect(mockMkdir).toHaveBeenNthCalledWith( 1, - expect.stringMatching(/src\/app$/), - expect.anything() - ) - expect(mockMkdir).toHaveBeenNthCalledWith( - 2, expect.stringMatching(/src\/lib$/), expect.anything() ) expect(mockMkdir).toHaveBeenNthCalledWith( - 3, + 2, expect.stringMatching(/src\/components$/), expect.anything() ) @@ -184,12 +179,6 @@ test("init config-partial", async () => { ) expect(mockWriteFile).toHaveBeenNthCalledWith( 2, - expect.stringMatching(/src\/assets\/css\/tailwind.css$/), - expect.stringContaining(`@tailwind base`), - "utf8" - ) - expect(mockWriteFile).toHaveBeenNthCalledWith( - 3, expect.stringMatching(/utils.ts$/), expect.stringContaining(`import { type ClassValue, clsx } from "clsx"`), "utf8" diff --git a/packages/cli/test/fixtures/config-full/src/app/globals.css b/packages/cli/test/fixtures/config-full/src/app/globals.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/packages/cli/test/fixtures/config-full/src/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/packages/cli/test/utils/updaters/update-tailwind-css.test.ts b/packages/cli/test/utils/updaters/update-tailwind-css.test.ts index 9f654cbb88..1d6173fe6c 100644 --- a/packages/cli/test/utils/updaters/update-tailwind-css.test.ts +++ b/packages/cli/test/utils/updaters/update-tailwind-css.test.ts @@ -34,6 +34,14 @@ describe("transformTailwindCss", () => { --background: black; --foreground: white } + } + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } " `) @@ -82,6 +90,74 @@ describe("transformTailwindCss", () => { --foreground: 60 9.1% 97.8%; } } + + @layer base { + + * { + + @apply border-border; + } + + body { + + @apply bg-background text-foreground; + } + } + " + `) + }) + + test("should not add the base layer if it is already present", async () => { + expect( + await transformTailwindCss( + `@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base{ + :root{ + --background: 210 40% 98%; + } + + .dark{ + --background: 222.2 84% 4.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + `, + {} + ) + ).toMatchInlineSnapshot(` + "@tailwind base; + @tailwind components; + @tailwind utilities; + + @layer base{ + :root{ + --background: 210 40% 98%; + } + + .dark{ + --background: 222.2 84% 4.9%; + } + } + + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + } " `) })