refactor(cli): move defaults out of cli

This commit is contained in:
shadcn
2024-08-18 13:57:29 +04:00
parent 33595c7d21
commit 0711a3711e
10 changed files with 445 additions and 111 deletions

View File

@@ -0,0 +1,37 @@
{
"name": "default",
"dependencies": [
"tailwindcss-animate",
"class-variance-authority",
"clsx",
"tailwind-merge",
"lucide-react",
""
],
"registryDependencies": [],
"tailwind": {
"config": {
"theme": {
"extend": {
"borderRadius": {
"lg": "var(--radius)",
"md": "calc(var(--radius) - 2px)",
"sm": "calc(var(--radius) - 4px)"
}
}
},
"plugins": [
"require(\"tailwindcss-animate\")"
]
}
},
"files": [],
"cssVariables": {
"light": {
"--radius": "0.5rem"
},
"dark": {
"--radius": "0.5rem"
}
}
}

View File

@@ -0,0 +1,37 @@
{
"name": "new-york",
"dependencies": [
"tailwindcss-animate",
"class-variance-authority",
"clsx",
"tailwind-merge",
"lucide-react",
"@radix-ui/react-icons"
],
"registryDependencies": [],
"tailwind": {
"config": {
"theme": {
"extend": {
"borderRadius": {
"lg": "var(--radius)",
"md": "calc(var(--radius) - 2px)",
"sm": "calc(var(--radius) - 4px)"
}
}
},
"plugins": [
"require(\"tailwindcss-animate\")"
]
}
},
"files": [],
"cssVariables": {
"light": {
"--radius": "0.5rem"
},
"dark": {
"--radius": "0.5rem"
}
}
}

View File

@@ -15,6 +15,14 @@ import { themes } from "../registry/themes"
const REGISTRY_PATH = path.join(process.cwd(), "public/registry")
const SHARED_DEPENDENCIES = [
"tailwindcss-animate",
"class-variance-authority",
"clsx",
"tailwind-merge",
"lucide-react",
]
// ----------------------------------------------------------------------------
// Build __registry__/index.tsx.
// ----------------------------------------------------------------------------
@@ -325,6 +333,54 @@ async function buildStyles(registry: Registry) {
)
}
// ----------------------------------------------------------------------------
// Build registry/styles/[name]/index.json.
// ----------------------------------------------------------------------------
async function buildStylesIndex() {
for (const style of styles) {
const targetPath = path.join(REGISTRY_PATH, "styles", style.name)
const payload = {
name: style.name,
dependencies: [
...SHARED_DEPENDENCIES,
// TODO: Remove this when we migrate to lucide-react.
style.name === "new-york" ? "@radix-ui/react-icons" : "",
],
registryDependencies: [],
tailwind: {
config: {
theme: {
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [`require("tailwindcss-animate")`],
},
},
files: [],
cssVariables: {
light: {
"--radius": "0.5rem",
},
dark: {
"--radius": "0.5rem",
},
},
}
await fs.writeFile(
path.join(targetPath, "index.json"),
JSON.stringify(payload, null, 2),
"utf8"
)
}
}
// ----------------------------------------------------------------------------
// Build registry/colors/index.json.
// ----------------------------------------------------------------------------
@@ -637,6 +693,7 @@ try {
await buildRegistry(result.data)
await buildStyles(result.data)
await buildStylesIndex()
await buildThemes()
console.log("✅ Done!")

View File

@@ -12,16 +12,29 @@ import {
} from "@/src/utils/get-config"
import { getProjectConfig } from "@/src/utils/get-project-info"
import { handleError } from "@/src/utils/handle-error"
import {
INITIAL_TAILWIND_CONFIG,
INITIAL_TAILWIND_CONFIG_WITH_CSS_VARIABLES,
} from "@/src/utils/initializers/defaults"
import { initializeDependencies } from "@/src/utils/initializers/initialize-dependencies"
import { initializeDestinations } from "@/src/utils/initializers/initialize-destinations"
import { initializeTailwindConfig } from "@/src/utils/initializers/initialize-tailwind-config"
import {
buildTailwindThemeColorsFromCssVars,
initializeTailwindConfig,
} from "@/src/utils/initializers/initialize-tailwind-config"
import { initializeTailwindCss } from "@/src/utils/initializers/initialize-tailwind-css"
import { initializeUtils } from "@/src/utils/initializers/initialize-utils"
import { logger } from "@/src/utils/logger"
import { preFlight } from "@/src/utils/preflight"
import { getRegistryBaseColors, getRegistryStyles } from "@/src/utils/registry"
import {
getRegistryBaseColor,
getRegistryBaseColors,
getRegistryStyleIndex,
getRegistryStyles,
} from "@/src/utils/registry"
import chalk from "chalk"
import { Command } from "commander"
import deepmerge from "deepmerge"
import ora from "ora"
import prompts from "prompts"
import { z } from "zod"
@@ -298,7 +311,42 @@ export async function runInit(config: Config) {
// Run initializers.
const initializersSpinner = ora(`Initializing project...`)?.start()
await initializeTailwindConfig(config)
const [payload, baseColor] = await Promise.all([
getRegistryStyleIndex(config.style),
getRegistryBaseColor(config.tailwind.baseColor),
])
// Inline the base color in the tailwind config.
if (config.tailwind.cssVariables) {
payload.cssVars = {
light: {
...baseColor.cssVars.light,
...payload.cssVars?.light,
},
dark: {
...baseColor.cssVars.dark,
...payload.cssVars?.dark,
},
}
// Move the css vars to the tailwind config.
if (payload.tailwind?.config) {
payload.tailwind.config = deepmerge(payload.tailwind.config, {
theme: {
extend: {
colors: buildTailwindThemeColorsFromCssVars(
baseColor.cssVars.light
),
},
},
})
}
}
if (payload.tailwind?.config) {
await initializeTailwindConfig(config, payload.tailwind?.config)
}
await initializeTailwindCss(config)
await initializeUtils(config)
initializersSpinner?.succeed()

View File

@@ -15,84 +15,17 @@ import {
VariableStatement,
} from "ts-morph"
type InitializerTailwindConfig = Omit<TailwindConfig, "plugins"> & {
export type InitializerTailwindConfig = Omit<TailwindConfig, "plugins"> & {
// We only want string plugins for now.
plugins?: string[]
}
const DEFAULT_TAILWIND_CONFIG = {
content: [],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [`require("tailwindcss-animate")`],
} satisfies InitializerTailwindConfig
export async function initializeTailwindConfig(config: Config) {
export async function initializeTailwindConfig(
config: Config,
tailwindConfig: InitializerTailwindConfig
) {
const raw = await fs.readFile(config.resolvedPaths.tailwindConfig, "utf8")
const output = await transformTailwindConfig(raw, DEFAULT_TAILWIND_CONFIG, {
const output = await transformTailwindConfig(raw, tailwindConfig, {
config,
})
await fs.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8")
@@ -219,6 +152,14 @@ async function addTailwindConfigTheme(
configObject: ObjectLiteralExpression,
theme: InitializerTailwindConfig["theme"]
) {
// Ensure there is a theme property.
if (!configObject.getProperty("theme")) {
configObject.addPropertyAssignment({
name: "theme",
initializer: "{}",
})
}
// Nest all spread properties.
nestSpreadProperties(configObject)
@@ -428,3 +369,41 @@ function parseValue(node: any): any {
return node.getText()
}
}
export function buildTailwindThemeColorsFromCssVars(
cssVars: Record<string, string>
) {
const result: Record<string, any> = {}
for (const key of Object.keys(cssVars)) {
const parts = key.split("-")
const colorName = parts[0]
const subType = parts.slice(1).join("-")
if (subType === "") {
if (typeof result[colorName] === "object") {
result[colorName].DEFAULT = `hsl(var(--${key}))`
} else {
result[colorName] = `hsl(var(--${key}))`
}
} else {
if (typeof result[colorName] !== "object") {
result[colorName] = { DEFAULT: `hsl(var(--${colorName}))` }
}
result[colorName][subType] = `hsl(var(--${key}))`
}
}
// Remove DEFAULT if it's not in the original cssVars
for (const [colorName, value] of Object.entries(result)) {
if (
typeof value === "object" &&
value.DEFAULT === `hsl(var(--${colorName}))` &&
!(colorName in cssVars)
) {
delete value.DEFAULT
}
}
return result
}

View File

@@ -36,6 +36,17 @@ export async function getRegistryStyles() {
}
}
export async function getRegistryStyleIndex(style: string) {
try {
const [result] = await fetchRegistry([`styles/${style}/index.json`])
return registryItemWithContentSchema.parse(result)
} catch (error) {
console.log(error)
throw new Error(`Failed to fetch style index from registry.`)
}
}
export async function getRegistryBaseColors() {
return [
{
@@ -125,7 +136,7 @@ export async function getItemTargetPath(
return config.resolvedPaths.ui
}
const [parent, type] = item.type.split(":")
const [parent, type] = item.type?.split(":") ?? []
if (!(parent in config.resolvedPaths)) {
return null
}

View File

@@ -7,7 +7,24 @@ export const registryItemSchema = z.object({
devDependencies: z.array(z.string()).optional(),
registryDependencies: z.array(z.string()).optional(),
files: z.array(z.string()),
type: z.enum(["components:ui", "components:component", "components:example"]),
type: z
.enum(["components:ui", "components:component", "components:example"])
.optional(),
tailwind: z
.object({
config: z.object({
content: z.array(z.string()).optional(),
theme: z.record(z.string(), z.any()).optional(),
plugins: z.array(z.string()).optional(),
}),
})
.optional(),
cssVars: z
.object({
light: z.record(z.string(), z.string()).optional(),
dark: z.record(z.string(), z.string()).optional(),
})
.optional(),
})
export const registryIndexSchema = z.array(registryItemSchema)
@@ -17,6 +34,11 @@ export const registryItemWithContentSchema = registryItemSchema.extend({
z.object({
name: z.string(),
content: z.string(),
type: z.enum([
"components:ui",
"components:component",
"components:example",
]),
})
),
})

View File

@@ -284,6 +284,33 @@ export default config
"
`;
exports[`transformTailwindConfig -> theme > should add theme if not in config 1`] = `
"import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
}
}
}
}
}
export default config
"
`;
exports[`transformTailwindConfig -> theme > should handle multiple properties 1`] = `
"import type { Config } from 'tailwindcss'

View File

@@ -0,0 +1,115 @@
import { describe, expect, test } from "vitest"
import { buildTailwindThemeColorsFromCssVars } from "../../../src/utils/initializers/initialize-tailwind-config"
describe("buildTailwindThemeColorsFromCssVars", () => {
test("should inline color names", () => {
expect(
buildTailwindThemeColorsFromCssVars({
primary: "blue",
"primary-light": "skyblue",
"primary-dark": "navy",
secondary: "green",
accent: "orange",
"accent-hover": "darkorange",
"accent-active": "orangered",
})
).toEqual({
primary: {
DEFAULT: "hsl(var(--primary))",
light: "hsl(var(--primary-light))",
dark: "hsl(var(--primary-dark))",
},
secondary: "hsl(var(--secondary))",
accent: {
DEFAULT: "hsl(var(--accent))",
hover: "hsl(var(--accent-hover))",
active: "hsl(var(--accent-active))",
},
})
})
test("should not add a DEFAULT if not present", () => {
expect(
buildTailwindThemeColorsFromCssVars({
"primary-light": "skyblue",
"primary-dark": "navy",
secondary: "green",
accent: "orange",
"accent-hover": "darkorange",
"accent-active": "orangered",
})
).toEqual({
primary: {
light: "hsl(var(--primary-light))",
dark: "hsl(var(--primary-dark))",
},
secondary: "hsl(var(--secondary))",
accent: {
DEFAULT: "hsl(var(--accent))",
hover: "hsl(var(--accent-hover))",
active: "hsl(var(--accent-active))",
},
})
})
test("should build tailwind theme colors from css vars", () => {
expect(
buildTailwindThemeColorsFromCssVars({
background: "0 0% 100%",
foreground: "224 71.4% 4.1%",
card: "0 0% 100%",
"card-foreground": "224 71.4% 4.1%",
popover: "0 0% 100%",
"popover-foreground": "224 71.4% 4.1%",
primary: "220.9 39.3% 11%",
"primary-foreground": "210 20% 98%",
secondary: "220 14.3% 95.9%",
"secondary-foreground": "220.9 39.3% 11%",
muted: "220 14.3% 95.9%",
"muted-foreground": "220 8.9% 46.1%",
accent: "220 14.3% 95.9%",
"accent-foreground": "220.9 39.3% 11%",
destructive: "0 84.2% 60.2%",
"destructive-foreground": "210 20% 98%",
border: "220 13% 91%",
input: "220 13% 91%",
ring: "224 71.4% 4.1%",
})
).toEqual({
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
})
})
})

View File

@@ -22,6 +22,7 @@ const SHARED_CONFIG = {
utils: "@/lib/utils",
},
resolvedPaths: {
cwd: ".",
tailwindConfig: "tailwind.config.ts",
tailwindCss: "app/globals.css",
components: "./components",
@@ -471,40 +472,40 @@ export default config
})
describe("transformTailwindConfig -> theme", () => {
// test("should add theme if not in config", async () => {
// expect(
// await transformTailwindConfig(
// `import type { Config } from 'tailwindcss'
test("should add theme if not in config", async () => {
expect(
await transformTailwindConfig(
`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}",
// ],
// }
// export default config
// `,
// {
// theme: {
// extend: {
// colors: {
// background: "hsl(var(--background))",
// foreground: "hsl(var(--foreground))",
// primary: {
// DEFAULT: "hsl(var(--primary))",
// foreground: "hsl(var(--primary-foreground))",
// },
// },
// },
// },
// },
// {
// config: SHARED_CONFIG,
// }
// )
// ).toMatchSnapshot()
// })
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
}
export default config
`,
{
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
},
},
},
},
{
config: SHARED_CONFIG,
}
)
).toMatchSnapshot()
})
test("should merge existing theme", async () => {
expect(