Skip to content

Commit 99ff9ca

Browse files
authored
fix(shadcn): add src to content in tailwind config (#4787)
* feat(shadcn): handle src dir * chore: changeset
1 parent cd9a55b commit 99ff9ca

6 files changed

Lines changed: 287 additions & 2 deletions

File tree

.changeset/kind-paws-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": patch
3+
---
4+
5+
add src to content for tailwind

packages/shadcn/src/commands/init.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { highlighter } from "@/src/utils/highlighter"
2020
import { logger } from "@/src/utils/logger"
2121
import { getRegistryBaseColors, getRegistryStyles } from "@/src/utils/registry"
2222
import { spinner } from "@/src/utils/spinner"
23+
import { updateTailwindContent } from "@/src/utils/updaters/update-tailwind-content"
2324
import { Command } from "commander"
2425
import prompts from "prompts"
2526
import { z } from "zod"
@@ -137,6 +138,18 @@ export async function runInit(
137138
options.isNewProject || projectInfo?.framework.name === "next-app",
138139
})
139140

141+
// If a new project is using src dir, let's update the tailwind content config.
142+
// TODO: Handle this per framework.
143+
if (options.isNewProject && options.srcDir) {
144+
await updateTailwindContent(
145+
["./src/**/*.{js,ts,jsx,tsx,mdx}"],
146+
fullConfig,
147+
{
148+
silent: options.silent,
149+
}
150+
)
151+
}
152+
140153
return fullConfig
141154
}
142155

packages/shadcn/src/utils/updaters/update-tailwind-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ function addTailwindConfigPlugin(
249249
return configObject
250250
}
251251

252-
async function _createSourceFile(input: string, config: Config | null) {
252+
export async function _createSourceFile(input: string, config: Config | null) {
253253
const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"))
254254
const resolvedPath =
255255
config?.resolvedPaths?.tailwindConfig || "tailwind.config.ts"
@@ -268,7 +268,7 @@ async function _createSourceFile(input: string, config: Config | null) {
268268
return sourceFile
269269
}
270270

271-
function _getQuoteChar(configObject: ObjectLiteralExpression) {
271+
export function _getQuoteChar(configObject: ObjectLiteralExpression) {
272272
return configObject
273273
.getFirstDescendantByKind(SyntaxKind.StringLiteral)
274274
?.getQuoteKind() === QuoteKind.Single
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { promises as fs } from "fs"
2+
import path from "path"
3+
import { Config } from "@/src/utils/get-config"
4+
import { highlighter } from "@/src/utils/highlighter"
5+
import { spinner } from "@/src/utils/spinner"
6+
import {
7+
_createSourceFile,
8+
_getQuoteChar,
9+
} from "@/src/utils/updaters/update-tailwind-config"
10+
import { ObjectLiteralExpression, SyntaxKind } from "ts-morph"
11+
12+
export async function updateTailwindContent(
13+
content: string[],
14+
config: Config,
15+
options: {
16+
silent?: boolean
17+
}
18+
) {
19+
if (!content) {
20+
return
21+
}
22+
23+
options = {
24+
silent: false,
25+
...options,
26+
}
27+
28+
const tailwindFileRelativePath = path.relative(
29+
config.resolvedPaths.cwd,
30+
config.resolvedPaths.tailwindConfig
31+
)
32+
const tailwindSpinner = spinner(
33+
`Updating ${highlighter.info(tailwindFileRelativePath)}`,
34+
{
35+
silent: options.silent,
36+
}
37+
).start()
38+
const raw = await fs.readFile(config.resolvedPaths.tailwindConfig, "utf8")
39+
const output = await transformTailwindContent(raw, content, config)
40+
await fs.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8")
41+
tailwindSpinner?.succeed()
42+
}
43+
44+
export async function transformTailwindContent(
45+
input: string,
46+
content: string[],
47+
config: Config
48+
) {
49+
const sourceFile = await _createSourceFile(input, config)
50+
// Find the object with content property.
51+
// This is faster than traversing the default export.
52+
// TODO: maybe we do need to traverse the default export?
53+
const configObject = sourceFile
54+
.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)
55+
.find((node) =>
56+
node
57+
.getProperties()
58+
.some(
59+
(property) =>
60+
property.isKind(SyntaxKind.PropertyAssignment) &&
61+
property.getName() === "content"
62+
)
63+
)
64+
65+
// We couldn't find the config object, so we return the input as is.
66+
if (!configObject) {
67+
return input
68+
}
69+
70+
addTailwindConfigContent(configObject, content)
71+
72+
return sourceFile.getFullText()
73+
}
74+
75+
async function addTailwindConfigContent(
76+
configObject: ObjectLiteralExpression,
77+
content: string[]
78+
) {
79+
const quoteChar = _getQuoteChar(configObject)
80+
81+
const existingProperty = configObject.getProperty("content")
82+
83+
if (!existingProperty) {
84+
const newProperty = {
85+
name: "content",
86+
initializer: `[${quoteChar}${content.join(
87+
`${quoteChar}, ${quoteChar}`
88+
)}${quoteChar}]`,
89+
}
90+
configObject.addPropertyAssignment(newProperty)
91+
92+
return configObject
93+
}
94+
95+
if (existingProperty.isKind(SyntaxKind.PropertyAssignment)) {
96+
const initializer = existingProperty.getInitializer()
97+
98+
// If property is an array, append.
99+
if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) {
100+
for (const contentItem of content) {
101+
const newValue = `${quoteChar}${contentItem}${quoteChar}`
102+
103+
// Check if the array already contains the value.
104+
if (
105+
initializer
106+
.getElements()
107+
.map((element) => element.getText())
108+
.includes(newValue)
109+
) {
110+
continue
111+
}
112+
113+
initializer.addElement(newValue)
114+
}
115+
}
116+
117+
return configObject
118+
}
119+
120+
return configObject
121+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`transformTailwindContent -> content property > should NOT add content property if already in config 1`] = `
4+
"import type { Config } from 'tailwindcss'
5+
6+
const config: Config = {
7+
content: [
8+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
9+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
10+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
11+
"./bar/**/*.{js,ts,jsx,tsx,mdx}"
12+
],
13+
theme: {
14+
extend: {
15+
backgroundImage: {
16+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
17+
"gradient-conic":
18+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
19+
},
20+
},
21+
},
22+
plugins: [],
23+
}
24+
export default config
25+
"
26+
`;
27+
28+
exports[`transformTailwindContent -> content property > should add content property if not in config 1`] = `
29+
"import type { Config } from 'tailwindcss'
30+
31+
const config: Config = {
32+
content: [
33+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
34+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
35+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
36+
"./foo/**/*.{js,ts,jsx,tsx,mdx}",
37+
"./bar/**/*.{js,ts,jsx,tsx,mdx}"
38+
],
39+
theme: {
40+
extend: {
41+
backgroundImage: {
42+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
43+
"gradient-conic":
44+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
45+
},
46+
},
47+
},
48+
plugins: [],
49+
}
50+
export default config
51+
"
52+
`;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, test } from "vitest"
2+
3+
import { transformTailwindContent } from "../../../src/utils/updaters/update-tailwind-content"
4+
5+
const SHARED_CONFIG = {
6+
$schema: "https://ui.shadcn.com/schema.json",
7+
style: "new-york",
8+
rsc: true,
9+
tsx: true,
10+
tailwind: {
11+
config: "tailwind.config.ts",
12+
css: "app/globals.css",
13+
baseColor: "slate",
14+
cssVariables: true,
15+
},
16+
aliases: {
17+
components: "@/components",
18+
utils: "@/lib/utils",
19+
},
20+
resolvedPaths: {
21+
cwd: ".",
22+
tailwindConfig: "tailwind.config.ts",
23+
tailwindCss: "app/globals.css",
24+
components: "./components",
25+
utils: "./lib/utils",
26+
ui: "./components/ui",
27+
},
28+
}
29+
30+
describe("transformTailwindContent -> content property", () => {
31+
test("should add content property if not in config", async () => {
32+
expect(
33+
await transformTailwindContent(
34+
`import type { Config } from 'tailwindcss'
35+
36+
const config: Config = {
37+
content: [
38+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
39+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
40+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
41+
],
42+
theme: {
43+
extend: {
44+
backgroundImage: {
45+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
46+
"gradient-conic":
47+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
48+
},
49+
},
50+
},
51+
plugins: [],
52+
}
53+
export default config
54+
`,
55+
["./foo/**/*.{js,ts,jsx,tsx,mdx}", "./bar/**/*.{js,ts,jsx,tsx,mdx}"],
56+
{
57+
config: SHARED_CONFIG,
58+
}
59+
)
60+
).toMatchSnapshot()
61+
})
62+
63+
test("should NOT add content property if already in config", async () => {
64+
expect(
65+
await transformTailwindContent(
66+
`import type { Config } from 'tailwindcss'
67+
68+
const config: Config = {
69+
content: [
70+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
71+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
72+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
73+
],
74+
theme: {
75+
extend: {
76+
backgroundImage: {
77+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
78+
"gradient-conic":
79+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
80+
},
81+
},
82+
},
83+
plugins: [],
84+
}
85+
export default config
86+
`,
87+
["./app/**/*.{js,ts,jsx,tsx,mdx}", "./bar/**/*.{js,ts,jsx,tsx,mdx}"],
88+
{
89+
config: SHARED_CONFIG,
90+
}
91+
)
92+
).toMatchSnapshot()
93+
})
94+
})

0 commit comments

Comments
 (0)