diff --git a/apps/v4/public/r/styles/new-york-v4/index.json b/apps/v4/public/r/styles/new-york-v4/index.json index ae7203d2fe..69f13cef90 100644 --- a/apps/v4/public/r/styles/new-york-v4/index.json +++ b/apps/v4/public/r/styles/new-york-v4/index.json @@ -16,7 +16,16 @@ "files": [], "cssVars": {}, "css": { - "@import \"shadcn/tailwind.css\"": {} + "@import \"tw-animate-css\"": {}, + "@import \"shadcn/tailwind.css\"": {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {} + }, + "body": { + "@apply bg-background text-foreground": {} + } + } }, "type": "registry:style" } \ No newline at end of file diff --git a/apps/v4/public/r/styles/new-york-v4/registry.json b/apps/v4/public/r/styles/new-york-v4/registry.json index 7b72e315e2..2db23814c3 100644 --- a/apps/v4/public/r/styles/new-york-v4/registry.json +++ b/apps/v4/public/r/styles/new-york-v4/registry.json @@ -19,7 +19,16 @@ "files": [], "cssVars": {}, "css": { - "@import \"shadcn/tailwind.css\"": {} + "@import \"tw-animate-css\"": {}, + "@import \"shadcn/tailwind.css\"": {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {} + }, + "body": { + "@apply bg-background text-foreground": {} + } + } }, "type": "registry:style" }, @@ -40,7 +49,16 @@ "files": [], "cssVars": {}, "css": { - "@import \"shadcn/tailwind.css\"": {} + "@import \"tw-animate-css\"": {}, + "@import \"shadcn/tailwind.css\"": {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {} + }, + "body": { + "@apply bg-background text-foreground": {} + } + } }, "type": "registry:style" }, diff --git a/apps/v4/public/r/styles/new-york-v4/style.json b/apps/v4/public/r/styles/new-york-v4/style.json index 452a4322f8..9d8bb6576c 100644 --- a/apps/v4/public/r/styles/new-york-v4/style.json +++ b/apps/v4/public/r/styles/new-york-v4/style.json @@ -16,7 +16,16 @@ "files": [], "cssVars": {}, "css": { - "@import \"shadcn/tailwind.css\"": {} + "@import \"tw-animate-css\"": {}, + "@import \"shadcn/tailwind.css\"": {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {} + }, + "body": { + "@apply bg-background text-foreground": {} + } + } }, "type": "registry:style" } \ No newline at end of file diff --git a/apps/v4/registry/new-york-v4/registry.ts b/apps/v4/registry/new-york-v4/registry.ts index d9a5b99690..ce71657675 100644 --- a/apps/v4/registry/new-york-v4/registry.ts +++ b/apps/v4/registry/new-york-v4/registry.ts @@ -26,7 +26,16 @@ const NEW_YORK_V4_STYLE = { devDependencies: ["tw-animate-css", "shadcn"], registryDependencies: ["utils"], css: { + '@import "tw-animate-css"': {}, '@import "shadcn/tailwind.css"': {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {}, + }, + body: { + "@apply bg-background text-foreground": {}, + }, + }, }, cssVars: {}, files: [], diff --git a/packages/shadcn/src/commands/add.ts b/packages/shadcn/src/commands/add.ts index ebfb821a5d..e2cc919418 100644 --- a/packages/shadcn/src/commands/add.ts +++ b/packages/shadcn/src/commands/add.ts @@ -90,20 +90,19 @@ export const add = new Command() } let itemType: z.infer | undefined - let shouldInstallBaseStyle = true + let shouldInstallStyleIndex = true if (components.length > 0) { const [registryItem] = await getRegistryItems([components[0]], { config: initialConfig, }) itemType = registryItem?.type - shouldInstallBaseStyle = - itemType !== "registry:theme" && itemType !== "registry:style" + shouldInstallStyleIndex = + itemType !== "registry:theme" && + itemType !== "registry:style" && + itemType !== "registry:base" if (isUniversalRegistryItem(registryItem)) { - await addComponents(components, initialConfig, { - ...options, - baseStyle: shouldInstallBaseStyle, - }) + await addComponents(components, initialConfig, options) return } @@ -180,8 +179,8 @@ export const add = new Command() isNewProject: false, srcDir: options.srcDir, cssVariables: options.cssVariables, - baseStyle: shouldInstallBaseStyle, - baseColor: shouldInstallBaseStyle ? undefined : "neutral", + installStyleIndex: shouldInstallStyleIndex, + baseColor: shouldInstallStyleIndex ? undefined : "neutral", components: options.components, }) initHasRun = true @@ -216,8 +215,8 @@ export const add = new Command() isNewProject: true, srcDir: options.srcDir, cssVariables: options.cssVariables, - baseStyle: shouldInstallBaseStyle, - baseColor: shouldInstallBaseStyle ? undefined : "neutral", + installStyleIndex: shouldInstallStyleIndex, + baseColor: shouldInstallStyleIndex ? undefined : "neutral", components: options.components, }) initHasRun = true @@ -244,10 +243,7 @@ export const add = new Command() config = updatedConfig if (!initHasRun) { - await addComponents(options.components, config, { - ...options, - baseStyle: shouldInstallBaseStyle, - }) + await addComponents(options.components, config, options) } // If we're adding a single component and it's from the v0 registry, diff --git a/packages/shadcn/src/commands/create.ts b/packages/shadcn/src/commands/create.ts index 53027d9515..53357cf6b4 100644 --- a/packages/shadcn/src/commands/create.ts +++ b/packages/shadcn/src/commands/create.ts @@ -204,7 +204,7 @@ export const create = new Command() rtl: opts.rtl, template, baseColor, - baseStyle: false, + installStyleIndex: false, registryBaseConfig, skipPreflight: false, }) @@ -218,7 +218,6 @@ export const create = new Command() components.push("direction") } await addComponents(components, config, { - baseStyle: false, silent: true, overwrite: true, }) diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index 9b12cd81f1..86abfbbab9 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -104,7 +104,7 @@ export const initOptionsSchema = z.object({ ).join("', '")}'`, } ), - baseStyle: z.boolean(), + installStyleIndex: z.boolean(), // Config from registry:base item to merge into components.json. registryBaseConfig: rawConfigSchema.deepPartial().optional(), }) @@ -157,6 +157,7 @@ export const init = new Command() isNewProject: false, components, ...opts, + installStyleIndex: opts.baseStyle, }) await loadEnvFiles(options.cwd) @@ -228,8 +229,8 @@ export const init = new Command() // Store config to be merged into components.json later. options.registryBaseConfig = item.config } - options.baseStyle = - item.extends === "none" ? false : options.baseStyle + options.installStyleIndex = + item.extends === "none" ? false : options.installStyleIndex } if (item?.type === "registry:style") { @@ -238,14 +239,13 @@ export const init = new Command() options.baseColor = "neutral" // If the style extends none, we don't want to install the base style. - options.baseStyle = - item.extends === "none" ? false : options.baseStyle + options.installStyleIndex = + item.extends === "none" ? false : options.installStyleIndex } } // If --no-base-style, we don't want to prompt for a base color either. - // The style will extend or override it. - if (!options.baseStyle) { + if (!options.installStyleIndex) { options.baseColor = "neutral" } @@ -326,7 +326,7 @@ export async function runInit( // Why index? Because when style is true, we read style from components.json and fetch that. // i.e new-york from components.json then fetch /styles/new-york/index. // TODO: Fix this so that we can extend any style i.e --style=new-york. - ...(options.baseStyle ? ["index"] : []), + ...(options.installStyleIndex ? ["index"] : []), ...(options.components ?? []), ] @@ -389,7 +389,6 @@ export async function runInit( // Init will always overwrite files. overwrite: true, silent: options.silent, - baseStyle: options.baseStyle, isNewProject: options.isNewProject || projectInfo?.framework.name === "next-app", }) diff --git a/packages/shadcn/src/utils/add-components.ts b/packages/shadcn/src/utils/add-components.ts index 3b6a4bac2d..38663a04a3 100644 --- a/packages/shadcn/src/utils/add-components.ts +++ b/packages/shadcn/src/utils/add-components.ts @@ -38,7 +38,6 @@ export async function addComponents( overwrite?: boolean silent?: boolean isNewProject?: boolean - baseStyle?: boolean registryHeaders?: Record> path?: string } @@ -47,7 +46,6 @@ export async function addComponents( overwrite: false, silent: false, isNewProject: false, - baseStyle: true, ...options, } @@ -74,11 +72,10 @@ async function addProjectComponents( overwrite?: boolean silent?: boolean isNewProject?: boolean - baseStyle?: boolean path?: string } ) { - if (!options.baseStyle && !components.length) { + if (!components.length) { return } @@ -117,7 +114,6 @@ async function addProjectComponents( tailwindVersion, tailwindConfig: tree.tailwind?.config, overwriteCssVars, - initIndex: options.baseStyle, }) // Add CSS updater @@ -157,11 +153,10 @@ async function addWorkspaceComponents( silent?: boolean isNewProject?: boolean isRemote?: boolean - baseStyle?: boolean path?: string } ) { - if (!options.baseStyle && !components.length) { + if (!components.length) { return } diff --git a/packages/shadcn/src/utils/updaters/update-css-vars.ts b/packages/shadcn/src/utils/updaters/update-css-vars.ts index f14af41638..fc1aea4109 100644 --- a/packages/shadcn/src/utils/updaters/update-css-vars.ts +++ b/packages/shadcn/src/utils/updaters/update-css-vars.ts @@ -5,7 +5,6 @@ import { registryItemTailwindSchema, } from "@/src/schema" import { Config } from "@/src/utils/get-config" -import { getPackageInfo } from "@/src/utils/get-package-info" import { TailwindVersion } from "@/src/utils/get-project-info" import { highlighter } from "@/src/utils/highlighter" import { spinner } from "@/src/utils/spinner" @@ -21,7 +20,6 @@ export async function updateCssVars( options: { cleanupDefaultNextStyles?: boolean overwriteCssVars?: boolean - initIndex?: boolean silent?: boolean tailwindVersion?: TailwindVersion tailwindConfig?: z.infer["config"] @@ -36,7 +34,6 @@ export async function updateCssVars( silent: false, tailwindVersion: "v3", overwriteCssVars: false, - initIndex: true, ...options, } const cssFilepath = config.resolvedPaths.tailwindCss @@ -56,7 +53,6 @@ export async function updateCssVars( tailwindVersion: options.tailwindVersion, tailwindConfig: options.tailwindConfig, overwriteCssVars: options.overwriteCssVars, - initIndex: options.initIndex, }) await fs.writeFile(cssFilepath, output, "utf8") cssVarsSpinner.succeed() @@ -71,13 +67,11 @@ export async function transformCssVars( tailwindVersion?: TailwindVersion tailwindConfig?: z.infer["config"] overwriteCssVars?: boolean - initIndex?: boolean } = { cleanupDefaultNextStyles: false, tailwindVersion: "v3", tailwindConfig: undefined, overwriteCssVars: false, - initIndex: false, } ) { options = { @@ -85,7 +79,6 @@ export async function transformCssVars( tailwindVersion: "v3", tailwindConfig: undefined, overwriteCssVars: false, - initIndex: false, ...options, } @@ -98,18 +91,6 @@ export async function transformCssVars( if (options.tailwindVersion === "v4") { plugins = [] - // Only add tw-animate-css if project does not have tailwindcss-animate - if (config.resolvedPaths?.cwd) { - const packageInfo = getPackageInfo(config.resolvedPaths.cwd) - if ( - !packageInfo?.dependencies?.["tailwindcss-animate"] && - !packageInfo?.devDependencies?.["tailwindcss-animate"] && - options.initIndex - ) { - plugins.push(addCustomImport({ params: "tw-animate-css" })) - } - } - plugins.push(addCustomVariant({ params: "dark (&:is(.dark *))" })) if (options.cleanupDefaultNextStyles) { @@ -130,12 +111,6 @@ export async function transformCssVars( } } - if (config.tailwind.cssVariables && options.initIndex) { - plugins.push( - updateBaseLayerPlugin({ tailwindVersion: options.tailwindVersion }) - ) - } - const result = await postcss(plugins).process(input, { from: undefined, }) @@ -151,81 +126,6 @@ export async function transformCssVars( return output } -function updateBaseLayerPlugin({ - tailwindVersion, -}: { - tailwindVersion?: TailwindVersion -}) { - return { - postcssPlugin: "update-base-layer", - Once(root: Root) { - const requiredRules = [ - { - selector: "*", - apply: - tailwindVersion === "v4" - ? "border-border outline-ring/50" - : "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: " ", before: "\n" }, - }) - root.append(baseLayer) - root.insertBefore(baseLayer, postcss.comment({ text: "---break---" })) - } - - 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, before: "\n " }, - }), - ], - raws: { semicolon: true, between: " ", before: "\n " }, - }) - ) - } - }) - }, - } -} - function updateCssVarsPlugin( cssVars: z.infer ) { @@ -662,13 +562,13 @@ function addCustomImport({ params }: { params: string }) { node.type === "atrule" && node.name === "import" ) - // Find custom variant node (to ensure we insert before it) + // Find custom variant node (to ensure we insert before it). const customVariantNode = root.nodes.find( (node): node is AtRule => node.type === "atrule" && node.name === "custom-variant" ) - // Check if our specific import already exists + // Check if our specific import already exists. const hasImport = importNodes.some( (node) => node.params.replace(/["']/g, "") === params ) @@ -681,18 +581,18 @@ function addCustomImport({ params }: { params: string }) { }) if (importNodes.length > 0) { - // If there are existing imports, add after the last import + // If there are existing imports, add after the last import. const lastImport = importNodes[importNodes.length - 1] root.insertAfter(lastImport, importNode) } else if (customVariantNode) { - // If no imports but has custom-variant, insert before it + // If no imports but has custom-variant, insert before it. root.insertBefore(customVariantNode, importNode) root.insertBefore( customVariantNode, postcss.comment({ text: "---break---" }) ) } else { - // If no imports and no custom-variant, insert at the start + // If no imports and no custom-variant, insert at the start. root.prepend(importNode) root.insertAfter(importNode, postcss.comment({ text: "---break---" })) } diff --git a/packages/shadcn/src/utils/updaters/update-css.ts b/packages/shadcn/src/utils/updaters/update-css.ts index 13b2c74357..37243225f5 100644 --- a/packages/shadcn/src/utils/updaters/update-css.ts +++ b/packages/shadcn/src/utils/updaters/update-css.ts @@ -472,12 +472,23 @@ function processRule(parent: Root | AtRule, selector: string, properties: any) { const atRuleMatch = prop.match(/@([a-zA-Z-]+)\s*(.*)/) if (atRuleMatch) { const [, atRuleName, atRuleParams] = atRuleMatch - const atRule = postcss.atRule({ - name: atRuleName, - params: atRuleParams, - raws: { semicolon: true, before: "\n " }, - }) - rule.append(atRule) + + // Check if this at-rule already exists in the rule. + const existingAtRule = rule.nodes?.find( + (node): node is AtRule => + node.type === "atrule" && + node.name === atRuleName && + node.params === atRuleParams + ) + + if (!existingAtRule) { + const atRule = postcss.atRule({ + name: atRuleName, + params: atRuleParams, + raws: { semicolon: true, before: "\n " }, + }) + rule.append(atRule) + } } } else if (typeof value === "string") { const decl = postcss.decl({ diff --git a/packages/shadcn/test/utils/updaters/update-css.test.ts b/packages/shadcn/test/utils/updaters/update-css.test.ts index c1d3a6fb5f..3470409922 100644 --- a/packages/shadcn/test/utils/updaters/update-css.test.ts +++ b/packages/shadcn/test/utils/updaters/update-css.test.ts @@ -853,6 +853,82 @@ describe("transformCss", () => { `) }) + test("should add base layer styles from registry:style css field", async () => { + const input = `@import "tailwindcss";` + + // This is the exact shape from the registry:style index item. + const result = await transformCss(input, { + '@import "tw-animate-css"': {}, + '@import "shadcn/tailwind.css"': {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {}, + }, + body: { + "@apply bg-background text-foreground": {}, + }, + }, + }) + + expect(result).toMatchInlineSnapshot(` + "@import "tailwindcss"; + @import "tw-animate-css"; + @import "shadcn/tailwind.css"; + + @layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + }" + `) + }) + + test("should not duplicate base layer styles if already present", async () => { + const input = `@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +}` + + const result = await transformCss(input, { + '@import "tw-animate-css"': {}, + '@import "shadcn/tailwind.css"': {}, + "@layer base": { + "*": { + "@apply border-border outline-ring/50": {}, + }, + body: { + "@apply bg-background text-foreground": {}, + }, + }, + }) + + expect(result).toMatchInlineSnapshot(` + "@import "tailwindcss"; + @import "tw-animate-css"; + @import "shadcn/tailwind.css"; + + @layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + }" + `) + }) + test("should replace existing keyframes instead of duplicating", async () => { const input = `@import "tailwindcss";