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(cli): add support for custom Tailwind prefix transformer #770

Merged
merged 6 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export async function promptForConfig(
const styles = await getRegistryStyles()
const baseColors = await getRegistryBaseColors()



Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty lines

const options = await prompts([
{
type: "select",
Expand Down Expand Up @@ -120,6 +122,12 @@ export async function promptForConfig(
active: "yes",
inactive: "no",
},
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this question should be moved down after tailwindConfig and if config exists, the default answer could be pre-populated based on config.

type: "text",
name: "tailwindPrefix",
message: `Do you want to use custom ${highlight("tailwind prefix")}? (if you don't use any, just leave it empty)`,
initial: "",
},
{
type: "text",
name: "tailwindConfig",
Expand Down Expand Up @@ -156,6 +164,7 @@ export async function promptForConfig(
css: options.tailwindCss,
baseColor: options.tailwindBaseColor,
cssVariables: options.tailwindCssVariables,
prefix: options.tailwindPrefix
},
rsc: options.rsc,
aliases: {
Expand Down Expand Up @@ -217,8 +226,8 @@ export async function runInit(cwd: string, config: Config) {
await fs.writeFile(
config.resolvedPaths.tailwindConfig,
config.tailwind.cssVariables
? templates.TAILWIND_CONFIG_WITH_VARIABLES
: templates.TAILWIND_CONFIG,
? templates.TAILWIND_CONFIG_WITH_VARIABLES(config.tailwind.prefix)
: templates.TAILWIND_CONFIG(config.tailwind.prefix),
"utf8"
)

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/utils/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const rawConfigSchema = z
css: z.string(),
baseColor: z.string(),
cssVariables: z.boolean().default(true),
prefix: z.string().default("").optional(),
}),
aliases: z.object({
components: z.string(),
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/utils/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function cn(...inputs: ClassValue[]) {
}
`

export const TAILWIND_CONFIG = `/** @type {import('tailwindcss').Config} */
export const TAILWIND_CONFIG = (prefix?: string) => `/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
Expand Down Expand Up @@ -40,10 +40,10 @@ module.exports = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [require("tailwindcss-animate")], ${prefix ? `\n prefix: "${prefix}",` : ""}
}`

export const TAILWIND_CONFIG_WITH_VARIABLES = `/** @type {import('tailwindcss').Config} */
export const TAILWIND_CONFIG_WITH_VARIABLES = (prefix?: string) => `/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
Expand Down Expand Up @@ -117,5 +117,5 @@ module.exports = {
},
},
},
plugins: [require("tailwindcss-animate")],
}`
plugins: [require("tailwindcss-animate")], ${prefix ? `\n prefix: "${prefix}",` : ""}
}`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if the empty end line was removed or it was like this before, but I'd suggest to keep it.
Also I see "prefix" option to be defined closer to the top of the config and not the end (maybe I'm mistaking and haven't seen enough configs in the wild). I think it's more visual on the top.

3 changes: 2 additions & 1 deletion packages/cli/src/utils/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { Project, ScriptKind, type SourceFile } from "ts-morph"
import * as z from "zod"
import { transformTwPrefixes } from "./transform-tw-prefix"

export type TransformOpts = {
filename: string
Expand All @@ -26,6 +27,7 @@ const transformers: Transformer[] = [
transformImport,
transformRsc,
transformCssVars,
transformTwPrefixes
]

const project = new Project({
Expand All @@ -46,6 +48,5 @@ export async function transform(opts: TransformOpts) {
for (const transformer of transformers) {
transformer({ sourceFile, ...opts })
}

return sourceFile.getFullText()
}
59 changes: 59 additions & 0 deletions packages/cli/src/utils/transformers/transform-tw-prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Transformer } from "@/src/utils/transformers"
import { SyntaxKind } from "ts-morph"
import { splitClassName } from "./transform-css-vars"

export const transformTwPrefixes: Transformer = async ({
sourceFile,
config,
}) => {
if (!config.tailwind || !config.tailwind.prefix) {
return sourceFile
}
sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((child) => {
const value = child.getText()
//this is a hack to prevent the removal of single space classes. If new single space classes are added to tailwind, they need to be added here
const KNOWN_SINGLE_SPACE_CLASSES = [
"border-b",
"flex",
"invisible",
"space-y-4",
"sr-only",
"[&_tr:last-child]:border-0",
"[&_tr]:border-b"
]
function checkIfKnownSingleSpaceClass(value: string) {
return KNOWN_SINGLE_SPACE_CLASSES.some((knownClass) =>
value.includes(knownClass)
)
}
if (value) {
TODO://this is very hacky way find tailwind classes but for current components it works. Need to find a better way
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meanwhile is there a better way?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also comment looks messed up.

if (value.split(" ").length > 1 && value.includes("-") || checkIfKnownSingleSpaceClass(value)) {
const valueWithColorMapping = applyTwPrefixes(
value.replace(/"/g, ""),
config.tailwind?.prefix
)
child.replaceWithText(`"${valueWithColorMapping.trim()}"`)
}
return
}
})
return sourceFile
}


export const applyTwPrefixes = (input: string, twPrefix: string) => {
const classNames = input.split(" ")
const prefixed: string[] = []
for (let className of classNames) {
const [variant, value, modifier] = splitClassName(className)
if (variant) {
modifier ? prefixed.push(`${variant}:${twPrefix}${value}/${modifier}`) :
prefixed.push(`${variant}:${twPrefix}${value}`)
} else {
modifier ? prefixed.push(`${twPrefix}${value}/${modifier}`) :
prefixed.push(`${twPrefix}${value}`)
}
}
return prefixed.join(" ")
}
3 changes: 2 additions & 1 deletion packages/cli/test/fixtures/config-full/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true
"cssVariables": true,
"prefix": "tw-"
},
"rsc": false,
"aliases": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`transform tailwind prefix 1`] = `
"import * as React from \\"react\\"
export function Foo() {
return <div className=\\"tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground\\">foo</div>
}\\"
"
`;

exports[`transform tailwind prefix 2`] = `
"import * as React from \\"react\\"
export function Foo() {
return <div className=\\"tw-bg-white hover:tw-bg-stone-100 tw-text-stone-50 sm:focus:tw-text-stone-900 dark:tw-bg-stone-950 dark:hover:tw-bg-stone-800 dark:tw-text-stone-900 dark:sm:focus:tw-text-stone-50\\">foo</div>
}\\"\\"
"
`;

exports[`transform tailwind prefix 3`] = `
"import * as React from \\"react\\"
export function Foo() {
return <div className={cn(\\"tw-bg-white hover:tw-bg-stone-100 dark:tw-bg-stone-950 dark:hover:tw-bg-stone-800\\", true && \\"tw-text-stone-50 sm:focus:tw-text-stone-900 dark:tw-text-stone-900 dark:sm:focus:tw-text-stone-50\\")}>foo</div>
}\\"\\"
"
`;
40 changes: 40 additions & 0 deletions packages/cli/test/utils/apply-prefix-mapping.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, test } from "vitest";
import { applyTwPrefixes } from "../../src/utils/transformers/transform-tw-prefix";

describe("apply tailwind prefix", () => {
test.each([
{
input: "bg-slate-800 text-gray-500",
output: "tw-bg-slate-800 tw-text-gray-500",
},
{
input: "hover:dark:bg-background dark:text-foreground",
output: "hover:dark:tw-bg-background dark:tw-text-foreground",
},
{
input: "rounded-lg border border-slate-200 bg-white text-slate-950 shadow-sm dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
output:
"tw-rounded-lg tw-border tw-border-slate-200 tw-bg-white tw-text-slate-950 tw-shadow-sm dark:tw-border-slate-800 dark:tw-bg-slate-950 dark:tw-text-slate-50",
},
{
input:
"text-red-500 border-red-500/50 dark:border-red-500 [&>svg]:text-red-500 text-red-500 dark:text-red-900 dark:border-red-900/50 dark:dark:border-red-900 dark:[&>svg]:text-red-900 dark:text-red-900",
output:
"tw-text-red-500 tw-border-red-500/50 dark:tw-border-red-500 [&>svg]:tw-text-red-500 tw-text-red-500 dark:tw-text-red-900 dark:tw-border-red-900/50 dark:dark:tw-border-red-900 dark:[&>svg]:tw-text-red-900 dark:tw-text-red-900",
},
{
input:
"flex h-full w-full items-center justify-center rounded-full bg-muted",
output:
"tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-rounded-full tw-bg-muted",
},
{
input:
"absolute right-4 top-4 bg-primary rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary",
output:
"tw-absolute tw-right-4 tw-top-4 tw-bg-primary tw-rounded-sm tw-opacity-70 tw-ring-offset-background tw-transition-opacity hover:tw-opacity-100 focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-ring focus:tw-ring-offset-2 disabled:tw-pointer-events-none data-[state=open]:tw-bg-secondary",
},
])(`applyTwPrefix($input) -> $output`, ({ input, output }) => {
expect(applyTwPrefixes(input, "tw-")).toBe(output)
})
})
1 change: 1 addition & 0 deletions packages/cli/test/utils/get-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ test("get config", async () => {
baseColor: "zinc",
css: "src/app/globals.css",
cssVariables: true,
prefix: "tw-"
},
aliases: {
components: "~/components",
Expand Down
76 changes: 76 additions & 0 deletions packages/cli/test/utils/transform-tw-prefix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, test } from "vitest"


import { transform } from "../../src/utils/transformers"
import stone from "../fixtures/colors/stone.json"

test("transform tailwind prefix", async () => {
expect(
await transform({
filename: "test.ts",
raw: `import * as React from "react"
export function Foo() {
return <div className="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">foo</div>
}"
`,
config: {
tailwind: {
baseColor: "stone",
prefix: "tw-"
},
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
baseColor: "stone",
})
).toMatchSnapshot()


expect(
await transform({
filename: "test.ts",
raw: `import * as React from "react"
export function Foo() {
return <div className="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">foo</div>
}"
`,
config: {
tailwind: {
baseColor: "stone",
cssVariables: false,
prefix: "tw-"
},
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
baseColor: stone,
})
).toMatchSnapshot()

expect(
await transform({
filename: "test.ts",
raw: `import * as React from "react"
export function Foo() {
return <div className={cn("bg-background hover:bg-muted", true && "text-primary-foreground sm:focus:text-accent-foreground")}>foo</div>
}"
`,
config: {
tailwind: {
baseColor: "stone",
cssVariables: false,
prefix: "tw-"
},
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
baseColor: stone,
})
).toMatchSnapshot()
})