diff --git a/.changeset/kind-paws-shop.md b/.changeset/kind-paws-shop.md new file mode 100644 index 0000000000..cbbbe65d50 --- /dev/null +++ b/.changeset/kind-paws-shop.md @@ -0,0 +1,5 @@ +--- +"shadcn": patch +--- + +add src to content for tailwind diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index a81a9831ad..74dc410f11 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -20,6 +20,7 @@ import { highlighter } from "@/src/utils/highlighter" import { logger } from "@/src/utils/logger" import { getRegistryBaseColors, getRegistryStyles } from "@/src/utils/registry" import { spinner } from "@/src/utils/spinner" +import { updateTailwindContent } from "@/src/utils/updaters/update-tailwind-content" import { Command } from "commander" import prompts from "prompts" import { z } from "zod" @@ -137,6 +138,18 @@ export async function runInit( options.isNewProject || projectInfo?.framework.name === "next-app", }) + // If a new project is using src dir, let's update the tailwind content config. + // TODO: Handle this per framework. + if (options.isNewProject && options.srcDir) { + await updateTailwindContent( + ["./src/**/*.{js,ts,jsx,tsx,mdx}"], + fullConfig, + { + silent: options.silent, + } + ) + } + return fullConfig } diff --git a/packages/shadcn/src/utils/updaters/update-tailwind-config.ts b/packages/shadcn/src/utils/updaters/update-tailwind-config.ts index 0deee35f77..f344d03643 100644 --- a/packages/shadcn/src/utils/updaters/update-tailwind-config.ts +++ b/packages/shadcn/src/utils/updaters/update-tailwind-config.ts @@ -249,7 +249,7 @@ function addTailwindConfigPlugin( return configObject } -async function _createSourceFile(input: string, config: Config | null) { +export 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" @@ -268,7 +268,7 @@ async function _createSourceFile(input: string, config: Config | null) { return sourceFile } -function _getQuoteChar(configObject: ObjectLiteralExpression) { +export function _getQuoteChar(configObject: ObjectLiteralExpression) { return configObject .getFirstDescendantByKind(SyntaxKind.StringLiteral) ?.getQuoteKind() === QuoteKind.Single diff --git a/packages/shadcn/src/utils/updaters/update-tailwind-content.ts b/packages/shadcn/src/utils/updaters/update-tailwind-content.ts new file mode 100644 index 0000000000..62f2a92300 --- /dev/null +++ b/packages/shadcn/src/utils/updaters/update-tailwind-content.ts @@ -0,0 +1,121 @@ +import { promises as fs } from "fs" +import path from "path" +import { Config } from "@/src/utils/get-config" +import { highlighter } from "@/src/utils/highlighter" +import { spinner } from "@/src/utils/spinner" +import { + _createSourceFile, + _getQuoteChar, +} from "@/src/utils/updaters/update-tailwind-config" +import { ObjectLiteralExpression, SyntaxKind } from "ts-morph" + +export async function updateTailwindContent( + content: string[], + config: Config, + options: { + silent?: boolean + } +) { + if (!content) { + return + } + + options = { + silent: false, + ...options, + } + + const tailwindFileRelativePath = path.relative( + config.resolvedPaths.cwd, + config.resolvedPaths.tailwindConfig + ) + const tailwindSpinner = spinner( + `Updating ${highlighter.info(tailwindFileRelativePath)}`, + { + silent: options.silent, + } + ).start() + const raw = await fs.readFile(config.resolvedPaths.tailwindConfig, "utf8") + const output = await transformTailwindContent(raw, content, config) + await fs.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8") + tailwindSpinner?.succeed() +} + +export async function transformTailwindContent( + input: string, + content: string[], + config: Config +) { + const sourceFile = await _createSourceFile(input, config) + // Find the object with content property. + // This is faster than traversing the default export. + // TODO: maybe we do need to traverse the default export? + const configObject = sourceFile + .getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression) + .find((node) => + node + .getProperties() + .some( + (property) => + property.isKind(SyntaxKind.PropertyAssignment) && + property.getName() === "content" + ) + ) + + // We couldn't find the config object, so we return the input as is. + if (!configObject) { + return input + } + + addTailwindConfigContent(configObject, content) + + return sourceFile.getFullText() +} + +async function addTailwindConfigContent( + configObject: ObjectLiteralExpression, + content: string[] +) { + const quoteChar = _getQuoteChar(configObject) + + const existingProperty = configObject.getProperty("content") + + if (!existingProperty) { + const newProperty = { + name: "content", + initializer: `[${quoteChar}${content.join( + `${quoteChar}, ${quoteChar}` + )}${quoteChar}]`, + } + configObject.addPropertyAssignment(newProperty) + + return configObject + } + + if (existingProperty.isKind(SyntaxKind.PropertyAssignment)) { + const initializer = existingProperty.getInitializer() + + // If property is an array, append. + if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) { + for (const contentItem of content) { + const newValue = `${quoteChar}${contentItem}${quoteChar}` + + // Check if the array already contains the value. + if ( + initializer + .getElements() + .map((element) => element.getText()) + .includes(newValue) + ) { + continue + } + + initializer.addElement(newValue) + } + } + + return configObject + } + + return configObject +} diff --git a/packages/shadcn/test/utils/updaters/__snapshots__/update-tailwind-content.test.ts.snap b/packages/shadcn/test/utils/updaters/__snapshots__/update-tailwind-content.test.ts.snap new file mode 100644 index 0000000000..6f82d6b72d --- /dev/null +++ b/packages/shadcn/test/utils/updaters/__snapshots__/update-tailwind-content.test.ts.snap @@ -0,0 +1,52 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transformTailwindContent -> content property > should NOT add content property if already in config 1`] = ` +"import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./bar/**/*.{js,ts,jsx,tsx,mdx}" + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +} +export default config + " +`; + +exports[`transformTailwindContent -> content property > should add content property if not in config 1`] = ` +"import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./foo/**/*.{js,ts,jsx,tsx,mdx}", + "./bar/**/*.{js,ts,jsx,tsx,mdx}" + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +} +export default config + " +`; diff --git a/packages/shadcn/test/utils/updaters/update-tailwind-content.test.ts b/packages/shadcn/test/utils/updaters/update-tailwind-content.test.ts new file mode 100644 index 0000000000..2aeb39b213 --- /dev/null +++ b/packages/shadcn/test/utils/updaters/update-tailwind-content.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, test } from "vitest" + +import { transformTailwindContent } from "../../../src/utils/updaters/update-tailwind-content" + +const SHARED_CONFIG = { + $schema: "https://ui.shadcn.com/schema.json", + style: "new-york", + rsc: true, + tsx: true, + tailwind: { + config: "tailwind.config.ts", + css: "app/globals.css", + baseColor: "slate", + cssVariables: true, + }, + aliases: { + components: "@/components", + utils: "@/lib/utils", + }, + resolvedPaths: { + cwd: ".", + tailwindConfig: "tailwind.config.ts", + tailwindCss: "app/globals.css", + components: "./components", + utils: "./lib/utils", + ui: "./components/ui", + }, +} + +describe("transformTailwindContent -> content property", () => { + test("should add content property if not in config", async () => { + expect( + await transformTailwindContent( + `import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +} +export default config + `, + ["./foo/**/*.{js,ts,jsx,tsx,mdx}", "./bar/**/*.{js,ts,jsx,tsx,mdx}"], + { + config: SHARED_CONFIG, + } + ) + ).toMatchSnapshot() + }) + + test("should NOT add content property if already in config", async () => { + expect( + await transformTailwindContent( + `import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +} +export default config + `, + ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./bar/**/*.{js,ts,jsx,tsx,mdx}"], + { + config: SHARED_CONFIG, + } + ) + ).toMatchSnapshot() + }) +})