Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: merge existing tailwind config #3558

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cli/package.json
Expand Up @@ -51,11 +51,13 @@
"chalk": "5.2.0",
"commander": "^10.0.0",
"cosmiconfig": "^8.1.3",
"defu": "6.1.4",
"diff": "^5.1.0",
"execa": "^7.0.0",
"fast-glob": "^3.3.2",
"fs-extra": "^11.1.0",
"https-proxy-agent": "^6.2.0",
"jiti": "1.21.0",
"lodash.template": "^4.5.0",
"node-fetch": "^3.3.0",
"ora": "^6.1.2",
Expand All @@ -72,6 +74,7 @@
"@types/lodash.template": "^4.5.1",
"@types/prompts": "^2.4.2",
"rimraf": "^4.1.3",
"tailwindcss": "^3.4.0",
"tsup": "^6.6.3",
"type-fest": "^3.8.0",
"typescript": "^4.9.3"
Expand Down
43 changes: 35 additions & 8 deletions packages/cli/src/commands/init.ts
Expand Up @@ -6,6 +6,7 @@ import {
DEFAULT_TAILWIND_CSS,
DEFAULT_UTILS,
getConfig,
getExistingTailwindConfig,
rawConfigSchema,
resolveConfigPaths,
type Config,
Expand All @@ -22,6 +23,7 @@ import {
import * as templates from "@/src/utils/templates"
import chalk from "chalk"
import { Command } from "commander"
import { defu } from "defu"
import { execa } from "execa"
import template from "lodash.template"
import ora from "ora"
Expand Down Expand Up @@ -334,21 +336,46 @@ export async function runInit(cwd: string, config: Config) {
config.resolvedPaths.tailwindConfig
)

let tailwindConfigTemplate: string
const existingTailwindConfig = getExistingTailwindConfig(
config.resolvedPaths.tailwindConfig
)

let tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_WITH_VARIABLES
: templates.TAILWIND_CONFIG
const mergedConfig = defu(tailwindConfigTemplate, existingTailwindConfig)
if (mergedConfig.prefix === "") delete mergedConfig.prefix
const stringedConfig = JSON.stringify(
mergedConfig,
(key, value) => {
if (key === "plugins") {
return [...value, "<%- animatePlugin %>"]
}
return value
},
2
)

let configStr: string
if (tailwindConfigExtension === ".ts") {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_TS_WITH_VARIABLES
: templates.TAILWIND_CONFIG_TS
configStr = `import type { Config } from "tailwindcss";
import tailwindAnimate from "tailwindcss-animate";

export default ${stringedConfig} satisfies Config`
configStr = configStr.replace('"<%- animatePlugin %>"', `tailwindAnimate`)
} else {
tailwindConfigTemplate = config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_WITH_VARIABLES
: templates.TAILWIND_CONFIG
configStr = `/** @type {import('tailwindcss').Config} */
module.exports = ${stringedConfig} satisfies Config`
configStr = configStr.replace(
"<%- animatePlugin %>",
`require("tailwindcss-animate")`
)
}

// Write tailwind config.
await fs.writeFile(
config.resolvedPaths.tailwindConfig,
template(tailwindConfigTemplate)({
template(configStr)({
extension,
prefix: config.tailwind.prefix,
}),
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/utils/get-config.ts
@@ -1,6 +1,9 @@
import { existsSync } from "fs"
import path from "path"
import { resolveImport } from "@/src/utils/resolve-import"
import { cosmiconfig } from "cosmiconfig"
import createJiti from "jiti"
import type { Config as TailwindConfig } from "tailwindcss"
import { loadConfig } from "tsconfig-paths"
import { z } from "zod"

Expand Down Expand Up @@ -101,3 +104,13 @@ export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
throw new Error(`Invalid configuration found in ${cwd}/components.json.`)
}
}

export function getExistingTailwindConfig(
resolvedPath: string
): TailwindConfig {
if (!existsSync(resolvedPath)) {
return { content: [] }
}
const dir = path.dirname(resolvedPath)
return createJiti(dir)(resolvedPath).default
}
156 changes: 18 additions & 138 deletions packages/cli/src/utils/templates.ts
@@ -1,3 +1,5 @@
import type { Config as TailwindConfig } from "tailwindcss"

export const UTILS = `import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

Expand All @@ -14,14 +16,13 @@ export function cn(...inputs) {
}
`

export const TAILWIND_CONFIG = `/** @type {import('tailwindcss').Config} */
module.exports = {
export const TAILWIND_CONFIG: TailwindConfig = {
darkMode: ["class"],
content: [
'./pages/**/*.{<%- extension %>,<%- extension %>x}',
'./components/**/*.{<%- extension %>,<%- extension %>x}',
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
"./pages/**/*.{<%- extension %>,<%- extension %>x}",
"./components/**/*.{<%- extension %>,<%- extension %>x}",
"./app/**/*.{<%- extension %>,<%- extension %>x}",
"./src/**/*.{<%- extension %>,<%- extension %>x}",
],
prefix: "<%- prefix %>",
theme: {
Expand Down Expand Up @@ -49,139 +50,19 @@ module.exports = {
},
},
},
plugins: [require("tailwindcss-animate")],
}`

export const TAILWIND_CONFIG_WITH_VARIABLES = `/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{<%- extension %>,<%- extension %>x}',
'./components/**/*.{<%- extension %>,<%- extension %>x}',
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
],
prefix: "<%- prefix %>",
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")],
}`

export const TAILWIND_CONFIG_TS = `import type { Config } from "tailwindcss"
// Added through transform before writing to file
// plugins: [require("tailwindcss-animate")],
}

const config = {
export const TAILWIND_CONFIG_WITH_VARIABLES: TailwindConfig = {
darkMode: ["class"],
content: [
'./pages/**/*.{<%- extension %>,<%- extension %>x}',
'./components/**/*.{<%- extension %>,<%- extension %>x}',
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
"./pages/**/*.{<%- extension %>,<%- extension %>x}",
"./components/**/*.{<%- extension %>,<%- extension %>x}",
"./app/**/*.{<%- extension %>,<%- extension %>x}",
"./src/**/*.{<%- extension %>,<%- extension %>x}",
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
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 Config

export default config`

export const TAILWIND_CONFIG_TS_WITH_VARIABLES = `import type { Config } from "tailwindcss"

const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{<%- extension %>,<%- extension %>x}',
'./components/**/*.{<%- extension %>,<%- extension %>x}',
'./app/**/*.{<%- extension %>,<%- extension %>x}',
'./src/**/*.{<%- extension %>,<%- extension %>x}',
],
prefix: "<%- prefix %>",
theme: {
container: {
center: true,
Expand Down Expand Up @@ -247,7 +128,6 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config

export default config`
// Added through transform before writing to file
// plugins: [require("tailwindcss-animate")],
}
51 changes: 50 additions & 1 deletion packages/cli/test/commands/init.test.ts
Expand Up @@ -4,7 +4,7 @@ import { execa } from "execa"
import { afterEach, expect, test, vi } from "vitest"

import { runInit } from "../../src/commands/init"
import { getConfig } from "../../src/utils/get-config"
import { Config, getConfig } from "../../src/utils/get-config"
import * as getPackageManger from "../../src/utils/get-package-manager"
import * as registry from "../../src/utils/registry"

Expand Down Expand Up @@ -155,6 +155,55 @@ test("init config-partial", async () => {
mockWriteFile.mockRestore()
})

test("init merges existing tailwind config", async () => {
vi.spyOn(getPackageManger, "getPackageManager").mockResolvedValue("npm")
vi.spyOn(registry, "getRegistryBaseColor").mockResolvedValue({
inlineColors: {},
cssVars: {},
inlineColorsTemplate:
"@tailwind base;\n@tailwind components;\n@tailwind utilities;\n",
cssVarsTemplate:
"@tailwind base;\n@tailwind components;\n@tailwind utilities;\n",
})
const mockMkdir = vi.spyOn(fs.promises, "mkdir").mockResolvedValue(undefined)
const mockWriteFile = vi.spyOn(fs.promises, "writeFile").mockResolvedValue()

const targetDir = path.resolve(__dirname, "../fixtures/t3-app")
await runInit(targetDir, {
resolvedPaths: {
tailwindConfig: targetDir + "/tailwind.config.ts",
tailwindCss: targetDir + "/src/styles/globals.css",
utils: targetDir + "/src/lib/utils.ts",
components: targetDir + "/src/components",
ui: targetDir + "/src/components",
},
tsx: true,
tailwind: {
config: targetDir + "/tailwind.config.ts",
css: targetDir + "/src/styles/globals.css",
baseColor: "slate",
cssVariables: true,
prefix: "tw-",
},
aliases: {
utils: "~/lib/utils",
components: "~/components",
},
rsc: true,
style: "default",
} satisfies Config)

expect(mockWriteFile).toHaveBeenNthCalledWith(
1,
expect.stringMatching(/tailwind.config.ts$/),
expect.stringContaining(`"fontFamily": {`),
"utf8"
)

mockMkdir.mockRestore()
mockWriteFile.mockRestore()
})

afterEach(() => {
vi.resetAllMocks()
})
13 changes: 9 additions & 4 deletions packages/cli/test/fixtures/t3-app/tailwind.config.ts
@@ -1,9 +1,14 @@
import { type Config } from "tailwindcss";
import { type Config } from "tailwindcss"
import { fontFamily } from "tailwindcss/defaultTheme"

export default {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
content: ["./src/**/*.tsx"],
theme: {
extend: {},
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
},
},
plugins: [],
} satisfies Config;
} satisfies Config