mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-29 15:44:22 +00:00
feat(cli): update tests
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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<typeof registryCssVarsSchema>
|
||||
) {
|
||||
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<typeof registryCssVarsSchema>) {
|
||||
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<string, string>
|
||||
) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
3
packages/cli/test/fixtures/config-full/src/app/globals.css
vendored
Normal file
3
packages/cli/test/fixtures/config-full/src/app/globals.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user