diff --git a/packages/cli/package.json b/packages/cli/package.json index 2f516a3653..87feef406f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -60,6 +60,7 @@ "lodash.template": "^4.5.0", "node-fetch": "^3.3.0", "ora": "^6.1.2", + "postcss": "^8.4.24", "prompts": "^2.4.2", "recast": "^0.23.2", "stringify-object": "^5.0.0", diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index b3ab476bab..fa38f7f968 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -340,7 +340,11 @@ export async function runInit(config: Config) { if (payload.tailwind?.config) { await updateTailwindConfig(payload.tailwind?.config, config) } - await updateTailwindCss(config) + + if (payload.cssVars) { + await updateTailwindCss(payload.cssVars, config) + } + await updateUtils(config) initializersSpinner?.succeed() diff --git a/packages/cli/src/utils/registry/schema.ts b/packages/cli/src/utils/registry/schema.ts index 8ab51f9865..06fe78f76d 100644 --- a/packages/cli/src/utils/registry/schema.ts +++ b/packages/cli/src/utils/registry/schema.ts @@ -1,5 +1,10 @@ import { z } from "zod" +export const registryCssVarsSchema = z.object({ + light: z.record(z.string(), z.string()).optional(), + dark: z.record(z.string(), z.string()).optional(), +}) + // TODO: Extract this to a shared package. export const registryItemSchema = z.object({ name: z.string(), @@ -19,12 +24,7 @@ export const registryItemSchema = z.object({ }), }) .optional(), - cssVars: z - .object({ - light: z.record(z.string(), z.string()).optional(), - dark: z.record(z.string(), z.string()).optional(), - }) - .optional(), + cssVars: registryCssVarsSchema.optional(), }) export const registryIndexSchema = z.array(registryItemSchema) diff --git a/packages/cli/src/utils/updaters/update-tailwind-css.ts b/packages/cli/src/utils/updaters/update-tailwind-css.ts index ed65d0ff93..a9da886a56 100644 --- a/packages/cli/src/utils/updaters/update-tailwind-css.ts +++ b/packages/cli/src/utils/updaters/update-tailwind-css.ts @@ -1,20 +1,127 @@ import { promises as fs } from "fs" import { Config } from "@/src/utils/get-config" -import { getRegistryBaseColor } from "@/src/utils/registry" -import { applyPrefixesCss } from "@/src/utils/transformers/transform-tw-prefix" +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" -export async function updateTailwindCss(config: Config) { - // Write css file. - const baseColor = await getRegistryBaseColor(config.tailwind.baseColor) - if (baseColor) { - await fs.writeFile( - config.resolvedPaths.tailwindCss, - config.tailwind.cssVariables - ? config.tailwind.prefix - ? applyPrefixesCss(baseColor.cssVarsTemplate, config.tailwind.prefix) - : baseColor.cssVarsTemplate - : baseColor.inlineColorsTemplate, - "utf8" - ) - } +export async function updateTailwindCss( + cssVars: z.infer, + config: Config +) { + const raw = await fs.readFile(config.resolvedPaths.tailwindCss, "utf8") + const output = await transformTailwindCss(raw, cssVars) + await fs.writeFile(config.resolvedPaths.tailwindCss, output, "utf8") +} + +export async function transformTailwindCss( + input: string, + cssVars: z.infer +) { + 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, { + from: undefined, + }) + + return result.css } diff --git a/packages/cli/test/fixtures/frameworks/remix/package.json b/packages/cli/test/fixtures/frameworks/remix/package.json index cfaeeb4899..c25a5c4149 100644 --- a/packages/cli/test/fixtures/frameworks/remix/package.json +++ b/packages/cli/test/fixtures/frameworks/remix/package.json @@ -31,7 +31,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "postcss": "^8.4.38", + "postcss": "^8.4.24", "tailwindcss": "^3.4.4", "typescript": "^5.1.6", "vite": "^5.1.0", diff --git a/packages/cli/test/fixtures/frameworks/t3-app/package.json b/packages/cli/test/fixtures/frameworks/t3-app/package.json index 49a3ff3a4f..8be1042dc9 100644 --- a/packages/cli/test/fixtures/frameworks/t3-app/package.json +++ b/packages/cli/test/fixtures/frameworks/t3-app/package.json @@ -26,7 +26,7 @@ "@typescript-eslint/parser": "^7.1.1", "eslint": "^8.57.0", "eslint-config-next": "^14.2.4", - "postcss": "^8.4.39", + "postcss": "^8.4.24", "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", "tailwindcss": "^3.4.3", diff --git a/packages/cli/test/fixtures/frameworks/t3-pages/package.json b/packages/cli/test/fixtures/frameworks/t3-pages/package.json index f6b9ae20df..5009f36ffe 100644 --- a/packages/cli/test/fixtures/frameworks/t3-pages/package.json +++ b/packages/cli/test/fixtures/frameworks/t3-pages/package.json @@ -26,7 +26,7 @@ "autoprefixer": "^10.4.14", "eslint": "^8.40.0", "eslint-config-next": "^13.4.2", - "postcss": "^8.4.21", + "postcss": "^8.4.24", "prettier": "^2.8.8", "prettier-plugin-tailwindcss": "^0.2.8", "tailwindcss": "3.3.7", diff --git a/packages/cli/test/fixtures/frameworks/vite/package-lock.json b/packages/cli/test/fixtures/frameworks/vite/package-lock.json index 5aecfd42e2..c8ffbc8c7b 100644 --- a/packages/cli/test/fixtures/frameworks/vite/package-lock.json +++ b/packages/cli/test/fixtures/frameworks/vite/package-lock.json @@ -21,7 +21,7 @@ "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", - "postcss": "^8.4.41", + "postcss": "^8.4.24", "tailwindcss": "^3.4.10", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0", diff --git a/packages/cli/test/utils/updaters/update-tailwind-css.test.ts b/packages/cli/test/utils/updaters/update-tailwind-css.test.ts new file mode 100644 index 0000000000..9f654cbb88 --- /dev/null +++ b/packages/cli/test/utils/updaters/update-tailwind-css.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "vitest" + +import { transformTailwindCss } from "../../../src/utils/updaters/update-tailwind-css" + +describe("transformTailwindCss", () => { + test("should add light and dark css vars if not present", async () => { + expect( + await transformTailwindCss( + `@tailwind base; +@tailwind components; +@tailwind utilities; + `, + { + light: { + background: "white", + foreground: "black", + }, + dark: { + background: "black", + foreground: "white", + }, + } + ) + ).toMatchInlineSnapshot(` + "@tailwind base; + @tailwind components; + @tailwind utilities; + @layer base{ + :root{ + --background: white; + --foreground: black + } + .dark{ + --background: black; + --foreground: white + } + } + " + `) + }) + + test("should update light and dark css vars if 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%; + } +} + `, + { + light: { + background: "215 20.2% 65.1%", + foreground: "222.2 84% 4.9%", + }, + dark: { + foreground: "60 9.1% 97.8%", + }, + } + ) + ).toMatchInlineSnapshot(` + "@tailwind base; + @tailwind components; + @tailwind utilities; + + @layer base{ + :root{ + --background: 215 20.2% 65.1%; + --foreground: 222.2 84% 4.9%; + } + + .dark{ + --background: 222.2 84% 4.9%; + --foreground: 60 9.1% 97.8%; + } + } + " + `) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 708f1755cd..335f2af23f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -424,6 +424,9 @@ importers: ora: specifier: ^6.1.2 version: 6.1.2 + postcss: + specifier: ^8.4.24 + version: 8.4.41 prompts: specifier: ^2.4.2 version: 2.4.2 @@ -5791,10 +5794,6 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.4.41: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} @@ -10974,13 +10973,13 @@ snapshots: eslint-plugin-tailwindcss@3.13.1(tailwindcss@3.3.7(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.5.3))): dependencies: fast-glob: 3.3.2 - postcss: 8.4.38 + postcss: 8.4.41 tailwindcss: 3.3.7(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.5.3)) eslint-plugin-tailwindcss@3.13.1(tailwindcss@3.3.7(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.3))): dependencies: fast-glob: 3.3.2 - postcss: 8.4.38 + postcss: 8.4.41 tailwindcss: 3.3.7(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.3)) eslint-plugin-turbo@1.9.9(eslint@8.44.0): @@ -13247,17 +13246,17 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-import@15.1.0(postcss@8.4.38): + postcss-import@15.1.0(postcss@8.4.41): dependencies: - postcss: 8.4.38 + postcss: 8.4.41 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.4.38): + postcss-js@4.0.1(postcss@8.4.41): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.38 + postcss: 8.4.41 postcss-load-config@3.1.4(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.14.0)(typescript@4.9.5)): dependencies: @@ -13267,25 +13266,25 @@ snapshots: postcss: 8.4.41 ts-node: 10.9.2(@types/node@20.14.0)(typescript@4.9.5) - postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.5.3)): + postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.5.3)): dependencies: lilconfig: 3.1.1 yaml: 2.4.3 optionalDependencies: - postcss: 8.4.38 + postcss: 8.4.41 ts-node: 10.9.2(@types/node@17.0.45)(typescript@5.5.3) - postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.3)): + postcss-load-config@4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.3)): dependencies: lilconfig: 3.1.1 yaml: 2.4.3 optionalDependencies: - postcss: 8.4.38 + postcss: 8.4.41 ts-node: 10.9.2(@types/node@20.11.27)(typescript@5.5.3) - postcss-nested@6.0.1(postcss@8.4.38): + postcss-nested@6.0.1(postcss@8.4.41): dependencies: - postcss: 8.4.38 + postcss: 8.4.41 postcss-selector-parser: 6.1.0 postcss-selector-parser@6.1.0: @@ -13307,12 +13306,6 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 - postcss@8.4.38: - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - postcss@8.4.41: dependencies: nanoid: 3.3.7 @@ -14216,11 +14209,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.1 - postcss: 8.4.38 - postcss-import: 15.1.0(postcss@8.4.38) - postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.5.3)) - postcss-nested: 6.0.1(postcss@8.4.38) + postcss: 8.4.41 + postcss-import: 15.1.0(postcss@8.4.41) + postcss-js: 4.0.1(postcss@8.4.41) + postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.5.3)) + postcss-nested: 6.0.1(postcss@8.4.41) postcss-selector-parser: 6.1.0 resolve: 1.22.8 sucrase: 3.35.0 @@ -14243,11 +14236,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.1 - postcss: 8.4.38 - postcss-import: 15.1.0(postcss@8.4.38) - postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.3)) - postcss-nested: 6.0.1(postcss@8.4.38) + postcss: 8.4.41 + postcss-import: 15.1.0(postcss@8.4.41) + postcss-js: 4.0.1(postcss@8.4.41) + postcss-load-config: 4.0.2(postcss@8.4.41)(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.3)) + postcss-nested: 6.0.1(postcss@8.4.41) postcss-selector-parser: 6.1.0 resolve: 1.22.8 sucrase: 3.35.0