-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Changes from 1 commit
148f9e1
39e7932
68a4247
b997c2c
095ed65
17f982a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,6 +85,8 @@ export async function promptForConfig( | |
const styles = await getRegistryStyles() | ||
const baseColors = await getRegistryBaseColors() | ||
|
||
|
||
|
||
const options = await prompts([ | ||
{ | ||
type: "select", | ||
|
@@ -120,6 +122,12 @@ export async function promptForConfig( | |
active: "yes", | ||
inactive: "no", | ||
}, | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
@@ -156,6 +164,7 @@ export async function promptForConfig( | |
css: options.tailwindCss, | ||
baseColor: options.tailwindBaseColor, | ||
cssVariables: options.tailwindCssVariables, | ||
prefix: options.tailwindPrefix | ||
}, | ||
rsc: options.rsc, | ||
aliases: { | ||
|
@@ -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" | ||
) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: [ | ||
|
@@ -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: [ | ||
|
@@ -117,5 +117,5 @@ module.exports = { | |
}, | ||
}, | ||
}, | ||
plugins: [require("tailwindcss-animate")], | ||
}` | ||
plugins: [require("tailwindcss-animate")], ${prefix ? `\n prefix: "${prefix}",` : ""} | ||
}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. meanwhile is there a better way? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(" ") | ||
} |
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> | ||
}\\"\\" | ||
" | ||
`; |
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) | ||
}) | ||
}) |
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() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty lines