From 0711a3711e5d97fe872512b3b2871ce71408b18f Mon Sep 17 00:00:00 2001 From: shadcn Date: Sun, 18 Aug 2024 13:57:29 +0400 Subject: [PATCH] refactor(cli): move defaults out of cli --- .../public/registry/styles/default/index.json | 37 ++++++ .../registry/styles/new-york/index.json | 37 ++++++ apps/www/scripts/build-registry.mts | 57 ++++++++ packages/cli/src/commands/init.ts | 54 +++++++- .../initialize-tailwind-config.ts | 125 ++++++++---------- packages/cli/src/utils/registry/index.ts | 13 +- packages/cli/src/utils/registry/schema.ts | 24 +++- .../initialize-tailwind-config.test.ts.snap | 27 ++++ ...ailwind-theme-colors-from-css-vars.test.ts | 115 ++++++++++++++++ .../initialize-tailwind-config.test.ts | 67 +++++----- 10 files changed, 445 insertions(+), 111 deletions(-) create mode 100644 apps/www/public/registry/styles/default/index.json create mode 100644 apps/www/public/registry/styles/new-york/index.json create mode 100644 packages/cli/test/utils/initializers/build-tailwind-theme-colors-from-css-vars.test.ts diff --git a/apps/www/public/registry/styles/default/index.json b/apps/www/public/registry/styles/default/index.json new file mode 100644 index 0000000000..ce444c7ec6 --- /dev/null +++ b/apps/www/public/registry/styles/default/index.json @@ -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" + } + } +} \ No newline at end of file diff --git a/apps/www/public/registry/styles/new-york/index.json b/apps/www/public/registry/styles/new-york/index.json new file mode 100644 index 0000000000..8c55e8b538 --- /dev/null +++ b/apps/www/public/registry/styles/new-york/index.json @@ -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" + } + } +} \ No newline at end of file diff --git a/apps/www/scripts/build-registry.mts b/apps/www/scripts/build-registry.mts index 7b37944d61..5640997c39 100644 --- a/apps/www/scripts/build-registry.mts +++ b/apps/www/scripts/build-registry.mts @@ -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!") diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 2fa1b0787b..cc75e5383b 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -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() diff --git a/packages/cli/src/utils/initializers/initialize-tailwind-config.ts b/packages/cli/src/utils/initializers/initialize-tailwind-config.ts index bb7365ccc2..0592c6616d 100644 --- a/packages/cli/src/utils/initializers/initialize-tailwind-config.ts +++ b/packages/cli/src/utils/initializers/initialize-tailwind-config.ts @@ -15,84 +15,17 @@ import { VariableStatement, } from "ts-morph" -type InitializerTailwindConfig = Omit & { +export type InitializerTailwindConfig = Omit & { // 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 +) { + const result: Record = {} + + 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 +} diff --git a/packages/cli/src/utils/registry/index.ts b/packages/cli/src/utils/registry/index.ts index a20c66d3b1..ce14a92138 100644 --- a/packages/cli/src/utils/registry/index.ts +++ b/packages/cli/src/utils/registry/index.ts @@ -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 } diff --git a/packages/cli/src/utils/registry/schema.ts b/packages/cli/src/utils/registry/schema.ts index b227590e11..8ab51f9865 100644 --- a/packages/cli/src/utils/registry/schema.ts +++ b/packages/cli/src/utils/registry/schema.ts @@ -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", + ]), }) ), }) diff --git a/packages/cli/test/utils/initializers/__snapshots__/initialize-tailwind-config.test.ts.snap b/packages/cli/test/utils/initializers/__snapshots__/initialize-tailwind-config.test.ts.snap index d30ee1d89a..2de3deeb8d 100644 --- a/packages/cli/test/utils/initializers/__snapshots__/initialize-tailwind-config.test.ts.snap +++ b/packages/cli/test/utils/initializers/__snapshots__/initialize-tailwind-config.test.ts.snap @@ -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' diff --git a/packages/cli/test/utils/initializers/build-tailwind-theme-colors-from-css-vars.test.ts b/packages/cli/test/utils/initializers/build-tailwind-theme-colors-from-css-vars.test.ts new file mode 100644 index 0000000000..08f873a8c2 --- /dev/null +++ b/packages/cli/test/utils/initializers/build-tailwind-theme-colors-from-css-vars.test.ts @@ -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))", + }, + }) + }) +}) diff --git a/packages/cli/test/utils/initializers/initialize-tailwind-config.test.ts b/packages/cli/test/utils/initializers/initialize-tailwind-config.test.ts index 5fc2cc2ce2..9d65b21054 100644 --- a/packages/cli/test/utils/initializers/initialize-tailwind-config.test.ts +++ b/packages/cli/test/utils/initializers/initialize-tailwind-config.test.ts @@ -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(