1384 lines
33 KiB
TypeScript
1384 lines
33 KiB
TypeScript
import { Project, SyntaxKind } from 'ts-morph'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildTailwindThemeColorsFromCssVars,
|
|
nestSpreadElements,
|
|
nestSpreadProperties,
|
|
transformTailwindConfig,
|
|
unnestSpreadProperties,
|
|
unnsetSpreadElements,
|
|
} from '../../../src/utils/updaters/update-tailwind-config'
|
|
|
|
const SHARED_CONFIG = {
|
|
$schema: 'https://shadcn-vue.com/schema.json',
|
|
style: 'new-york',
|
|
rsc: true,
|
|
tsx: true,
|
|
tailwind: {
|
|
config: 'tailwind.config.ts',
|
|
css: 'app/globals.css',
|
|
baseColor: 'slate',
|
|
cssVariables: true,
|
|
},
|
|
aliases: {
|
|
components: '@/components',
|
|
utils: '@/lib/utils',
|
|
},
|
|
resolvedPaths: {
|
|
cwd: '.',
|
|
tailwindConfig: 'tailwind.config.ts',
|
|
tailwindCss: 'app/globals.css',
|
|
components: './components',
|
|
utils: './lib/utils',
|
|
ui: './components/ui',
|
|
},
|
|
}
|
|
|
|
describe('transformTailwindConfig -> darkMode property', () => {
|
|
it('should add darkMode property 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`/** @type {import('tailwindcss').Config} */
|
|
|
|
export default {
|
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
|
theme: {
|
|
extend: {},
|
|
},
|
|
plugins: [],
|
|
}
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`/** @type {import('tailwindcss').Config} */
|
|
const foo = {
|
|
bar: 'baz',
|
|
}
|
|
|
|
export default {
|
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
|
theme: {
|
|
extend: {},
|
|
},
|
|
plugins: [],
|
|
}
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should append class to darkMode property if existing array', async () => {
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`import type { Config } from 'tailwindcss'
|
|
|
|
const config: Config = {
|
|
darkMode: ["selector"],
|
|
content: [
|
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should preserve quote kind', async () => {
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`import type { Config } from 'tailwindcss'
|
|
|
|
const config: Config = {
|
|
darkMode: ['selector', '[data-mode="dark"]'],
|
|
content: [
|
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should convert string to array and add class if darkMode is string', async () => {
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`import type { Config } from 'tailwindcss'
|
|
|
|
const config: Config = {
|
|
darkMode: "selector",
|
|
content: [
|
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should work with multiple darkMode selectors', async () => {
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`import type { Config } from 'tailwindcss'
|
|
|
|
const config: Config = {
|
|
darkMode: ['variant', [
|
|
'@media (prefers-color-scheme: dark) { &:not(.light *) }',
|
|
'&:is(.dark *)',
|
|
]],
|
|
content: [
|
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should not add darkMode property if already in config', async () => {
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`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: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
|
|
expect(
|
|
await transformTailwindConfig(
|
|
`import type { Config } from 'tailwindcss'
|
|
|
|
const config: Config = {
|
|
darkMode: ['class', 'selector'],
|
|
content: [
|
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
properties: [
|
|
{
|
|
name: 'darkMode',
|
|
value: 'class',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
})
|
|
|
|
describe('transformTailwindConfig -> plugin', () => {
|
|
it('should add plugin 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
plugins: ['require("tailwindcss-animate")'],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should append plugin to existing array', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [require("@tailwindcss/typography")],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
plugins: ['require("tailwindcss-animate")'],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should not add plugin if already 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
backgroundImage: {
|
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
"gradient-conic":
|
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
},
|
|
},
|
|
},
|
|
plugins: [require("@tailwindcss/typography"), require("tailwindcss-animate")],
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
plugins: ['require(\'tailwindcss-animate\')'],
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
})
|
|
|
|
describe('transformTailwindConfig -> theme', () => {
|
|
it('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()
|
|
})
|
|
|
|
it('should merge existing theme', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: [
|
|
"ui-sans-serif",
|
|
"sans-serif",
|
|
],
|
|
},
|
|
colors: {
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-foreground))',
|
|
},
|
|
card: {
|
|
DEFAULT: 'hsl(var(--card))',
|
|
foreground: 'hsl(var(--card-foreground))',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should keep spread assignments', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
...defaultColors,
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-foreground))',
|
|
},
|
|
card: {
|
|
DEFAULT: 'hsl(var(--card))',
|
|
foreground: 'hsl(var(--card-foreground))',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should handle multiple properties', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ["var(--font-geist-sans)", ...fontFamily.sans],
|
|
mono: ["var(--font-mono)", ...fontFamily.mono],
|
|
},
|
|
colors: {
|
|
...defaultColors,
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
},
|
|
boxShadow: {
|
|
...defaultBoxShadow,
|
|
"3xl": "0 35px 60px -15px rgba(0, 0, 0, 0.3)",
|
|
},
|
|
borderRadius: {
|
|
"3xl": "2rem",
|
|
},
|
|
animation: {
|
|
...defaultAnimation,
|
|
"spin-slow": "spin 3s linear infinite",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
heading: ['var(--font-geist-sans)'],
|
|
},
|
|
colors: {
|
|
border: 'hsl(var(--border))',
|
|
input: 'hsl(var(--input))',
|
|
ring: 'hsl(var(--ring))',
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-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)',
|
|
},
|
|
animation: {
|
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should not make any updates running on already updated config', async () => {
|
|
const input = `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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ["var(--font-geist-sans)", ...fontFamily.sans],
|
|
mono: ["var(--font-mono)", ...fontFamily.mono],
|
|
},
|
|
colors: {
|
|
...defaultColors,
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
},
|
|
boxShadow: {
|
|
...defaultBoxShadow,
|
|
"3xl": "0 35px 60px -15px rgba(0, 0, 0, 0.3)",
|
|
},
|
|
borderRadius: {
|
|
"3xl": "2rem",
|
|
},
|
|
animation: {
|
|
...defaultAnimation,
|
|
"spin-slow": "spin 3s linear infinite",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`
|
|
|
|
const tailwindConfig = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
heading: ['var(--font-geist-sans)'],
|
|
},
|
|
colors: {
|
|
border: 'hsl(var(--border))',
|
|
input: 'hsl(var(--input))',
|
|
ring: 'hsl(var(--ring))',
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-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)',
|
|
},
|
|
animation: {
|
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const output1 = await transformTailwindConfig(input, tailwindConfig, {
|
|
config: SHARED_CONFIG,
|
|
})
|
|
|
|
const output2 = await transformTailwindConfig(output1, tailwindConfig, {
|
|
config: SHARED_CONFIG,
|
|
})
|
|
|
|
const output3 = await transformTailwindConfig(output2, tailwindConfig, {
|
|
config: SHARED_CONFIG,
|
|
})
|
|
|
|
expect(output3).toBe(output1)
|
|
expect(output3).toBe(output2)
|
|
})
|
|
|
|
it('should keep quotes in strings', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
|
|
},
|
|
colors: {
|
|
...defaultColors,
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-foreground))',
|
|
},
|
|
card: {
|
|
DEFAULT: 'hsl(var(--card))',
|
|
foreground: 'hsl(var(--card-foreground))',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should keep arrays when formatted on multilines', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: [
|
|
'Figtree',
|
|
...defaultTheme.fontFamily.sans
|
|
],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
mono: ['Foo'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should handle objects nested in arrays', 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}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontSize: {
|
|
xs: ['0.75rem', { lineHeight: '1rem' }],
|
|
sm: [
|
|
'0.875rem',
|
|
{
|
|
lineHeight: '1.25rem',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{
|
|
theme: {
|
|
extend: {
|
|
fontSize: {
|
|
xl: [
|
|
'clamp(1.5rem, 1.04vi + 1.17rem, 2rem)',
|
|
{
|
|
lineHeight: '1.2',
|
|
letterSpacing: '-0.02em',
|
|
fontWeight: '600',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('should preserve boolean values', 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}",
|
|
],
|
|
theme: {
|
|
container: {
|
|
center: true
|
|
}
|
|
},
|
|
}
|
|
export default config
|
|
`,
|
|
{},
|
|
{
|
|
config: SHARED_CONFIG,
|
|
},
|
|
),
|
|
).toMatchSnapshot()
|
|
})
|
|
})
|
|
|
|
describe('nestSpreadProperties', () => {
|
|
let project: Project
|
|
|
|
beforeEach(() => {
|
|
project = new Project({ useInMemoryFileSystem: true })
|
|
})
|
|
|
|
function testTransformation(input: string, expected: string) {
|
|
const sourceFile = project.createSourceFile(
|
|
'test.ts',
|
|
`const config = ${input};`,
|
|
)
|
|
const configObject = sourceFile.getFirstDescendantByKind(
|
|
SyntaxKind.ObjectLiteralExpression,
|
|
)
|
|
if (!configObject)
|
|
throw new Error('Config object not found')
|
|
|
|
nestSpreadProperties(configObject)
|
|
|
|
const result = configObject.getText()
|
|
expect(result.replace(/\s+/g, '')).toBe(expected.replace(/\s+/g, ''))
|
|
}
|
|
|
|
it('should nest spread properties', () => {
|
|
testTransformation(
|
|
`{ theme: { ...foo, bar: { ...baz, one: "two" }, other: { a: "b", ...c } } }`,
|
|
`{ theme: { "___foo": "...foo", bar: { "___baz": "...baz", one: "two" }, other: { a: "b", "___c": "...c" } } }`,
|
|
)
|
|
})
|
|
|
|
it('should handle mixed property assignments', () => {
|
|
testTransformation(
|
|
`{ ...foo, a: 1, b() {}, ...bar, c: { ...baz } }`,
|
|
`{ "___foo": "...foo", a: 1, b() {}, "___bar": "...bar", c: { "___baz": "...baz" } }`,
|
|
)
|
|
})
|
|
|
|
it('should handle objects with only spread properties', () => {
|
|
testTransformation(
|
|
`{ ...foo, ...bar, ...baz }`,
|
|
`{ "___foo": "...foo", "___bar": "...bar", "___baz": "...baz" }`,
|
|
)
|
|
})
|
|
|
|
it('should handle property name conflicts', () => {
|
|
testTransformation(`{ foo: 1, ...foo }`, `{ foo: 1, "___foo": "...foo" }`)
|
|
})
|
|
|
|
it('should handle shorthand property names', () => {
|
|
testTransformation(`{ a, ...foo, b }`, `{ a, "___foo": "...foo", b }`)
|
|
})
|
|
|
|
it('should handle computed property names', () => {
|
|
testTransformation(
|
|
`{ ["computed"]: 1, ...foo }`,
|
|
`{ ["computed"]: 1, "___foo": "...foo" }`,
|
|
)
|
|
})
|
|
|
|
it('should handle spreads in arrays', () => {
|
|
testTransformation(
|
|
`{ foo: [{ ...bar }] }`,
|
|
`{ foo: [{ "___bar": "...bar" }] }`,
|
|
)
|
|
})
|
|
|
|
it('should handle deep nesting in arrays', () => {
|
|
testTransformation(
|
|
`{ foo: [{ baz: { ...other.baz }, ...bar }] }`,
|
|
`{ foo: [{ baz: { "___other.baz": "...other.baz" }, "___bar": "...bar" }] }`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('nestSpreadElements', () => {
|
|
let project: Project
|
|
|
|
beforeEach(() => {
|
|
project = new Project({ useInMemoryFileSystem: true })
|
|
})
|
|
|
|
function testTransformation(input: string, expected: string) {
|
|
const sourceFile = project.createSourceFile(
|
|
'test.ts',
|
|
`const config = ${input};`,
|
|
)
|
|
const configObject = sourceFile.getFirstDescendantByKind(
|
|
SyntaxKind.ArrayLiteralExpression,
|
|
)
|
|
if (!configObject)
|
|
throw new Error('Config object not found')
|
|
|
|
nestSpreadElements(configObject)
|
|
|
|
const result = configObject.getText()
|
|
expect(result.replace(/\s+/g, '')).toBe(expected.replace(/\s+/g, ''))
|
|
}
|
|
|
|
it('should spread elements', () => {
|
|
testTransformation(
|
|
`[...bar]`,
|
|
`["...bar"]`,
|
|
)
|
|
})
|
|
|
|
it('should handle mixed element types', () => {
|
|
testTransformation(
|
|
`['foo', 2, true, ...bar, "baz"]`,
|
|
`['foo', 2, true, "...bar", "baz"]`,
|
|
)
|
|
})
|
|
|
|
it('should handle arrays with only spread elements', () => {
|
|
testTransformation(
|
|
`[...foo, ...foo.bar, ...baz]`,
|
|
`["...foo", "...foo.bar", "...baz"]`,
|
|
)
|
|
})
|
|
|
|
it('should handle nested arrays with spreads', () => {
|
|
testTransformation(
|
|
`[...foo, [...bar]]`,
|
|
`["...foo", ["...bar"]]`,
|
|
)
|
|
})
|
|
|
|
it('should handle nested arrays within objects', () => {
|
|
testTransformation(
|
|
`[{ foo: [...foo] }]`,
|
|
`[{ foo: ["...foo"] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle deeply nested arrays within spread objects', () => {
|
|
testTransformation(
|
|
`[{ foo: [...foo, { bar: ['bar', ...bar ]}] }]`,
|
|
`[{ foo: ["...foo", { bar: ['bar', "...bar" ]}] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle optional paths in spread', () => {
|
|
testTransformation(
|
|
`[{ foo: [...foo?.bar] }]`,
|
|
`[{ foo: ["...foo?.bar"] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle computed property paths within spread', () => {
|
|
testTransformation(
|
|
`[{ foo: [...foo["bar"]] }]`,
|
|
`[{ foo: ["...foo["bar"]"] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle indexed paths in spread', () => {
|
|
testTransformation(
|
|
`[{ foo: [...foo[0]] }]`,
|
|
`[{ foo: ["...foo[0]"] }]`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('unnestSpreadProperties', () => {
|
|
let project: Project
|
|
|
|
beforeEach(() => {
|
|
project = new Project({ useInMemoryFileSystem: true })
|
|
})
|
|
|
|
function testTransformation(input: string, expected: string) {
|
|
const sourceFile = project.createSourceFile(
|
|
'test.ts',
|
|
`const config = ${input};`,
|
|
)
|
|
const configObject = sourceFile.getFirstDescendantByKind(
|
|
SyntaxKind.ObjectLiteralExpression,
|
|
)
|
|
if (!configObject)
|
|
throw new Error('Config object not found')
|
|
|
|
unnestSpreadProperties(configObject)
|
|
|
|
const result = configObject.getText()
|
|
expect(result.replace(/\s+/g, '')).toBe(expected.replace(/\s+/g, ''))
|
|
}
|
|
|
|
it('should nest spread properties', () => {
|
|
testTransformation(
|
|
`{ theme: { ___foo: "...foo", bar: { ___baz: "...baz", one: "two" }, other: { a: "b", ___c: "...c" } } }`,
|
|
`{ theme: { ...foo, bar: { ...baz, one: "two" }, other: { a: "b", ...c } } }`,
|
|
)
|
|
})
|
|
|
|
it('should handle mixed property assignments', () => {
|
|
testTransformation(
|
|
`{ ___foo: "...foo", a: 1, b() {}, ___bar: "...bar", c: { ___baz: "...baz" } }`,
|
|
`{ ...foo, a: 1, b() {}, ...bar, c: { ...baz } }`,
|
|
)
|
|
})
|
|
|
|
it('should handle objects with only spread properties', () => {
|
|
testTransformation(
|
|
`{ ___foo: "...foo", ___bar: "...bar", ___baz: "...baz" }`,
|
|
`{ ...foo, ...bar, ...baz }`,
|
|
)
|
|
})
|
|
|
|
it('should handle property name conflicts', () => {
|
|
testTransformation(`{ foo: 1, ___foo: "...foo" }`, `{ foo: 1, ...foo }`)
|
|
})
|
|
|
|
it('should handle shorthand property names', () => {
|
|
testTransformation(`{ a, ___foo: "...foo", b }`, `{ a, ...foo, b }`)
|
|
})
|
|
|
|
it('should handle computed property names', () => {
|
|
testTransformation(
|
|
`{ ["computed"]: 1, "___foo": "...foo" }`,
|
|
`{ ["computed"]: 1, ...foo }`,
|
|
)
|
|
})
|
|
|
|
it('should handle spread objects within arrays', () => {
|
|
testTransformation(
|
|
`{ ["computed"]: 1, foo: [{ "___foo": "...foo" }] }`,
|
|
`{ ["computed"]: 1, foo: [{...foo}] }`,
|
|
)
|
|
})
|
|
|
|
it('should handle deeply nested spread objects within an array', () => {
|
|
testTransformation(
|
|
`{ ["computed"]: 1, foo: [{ "___foo": "...foo", bar: { baz: 'baz', "___foo.bar": "...foo.bar" } }] }`,
|
|
`{ ["computed"]: 1, foo: [{...foo, bar: { baz: 'baz', ...foo.bar } }] }`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('unnestSpreadElements', () => {
|
|
let project: Project
|
|
|
|
beforeEach(() => {
|
|
project = new Project({ useInMemoryFileSystem: true })
|
|
})
|
|
|
|
function testTransformation(input: string, expected: string) {
|
|
const sourceFile = project.createSourceFile(
|
|
'test.ts',
|
|
`const config = ${input};`,
|
|
)
|
|
const configObject = sourceFile.getFirstDescendantByKind(
|
|
SyntaxKind.ArrayLiteralExpression,
|
|
)
|
|
if (!configObject)
|
|
throw new Error('Config object not found')
|
|
|
|
unnsetSpreadElements(configObject)
|
|
|
|
const result = configObject.getText()
|
|
expect(result.replace(/\s+/g, '')).toBe(expected.replace(/\s+/g, ''))
|
|
}
|
|
|
|
it('should spread elements', () => {
|
|
testTransformation(
|
|
`["...bar"]`,
|
|
`[...bar]`,
|
|
)
|
|
})
|
|
|
|
it('should handle mixed element types', () => {
|
|
testTransformation(
|
|
`['foo', 2, true, "...bar", "baz"]`,
|
|
`['foo', 2, true, ...bar, "baz"]`,
|
|
)
|
|
})
|
|
|
|
it('should handle arrays with only spread elements', () => {
|
|
testTransformation(
|
|
`["...foo", "...foo.bar", "...baz"]`,
|
|
`[...foo, ...foo.bar, ...baz]`,
|
|
)
|
|
})
|
|
|
|
it('should handle nested arrays with spreads', () => {
|
|
testTransformation(
|
|
`["...foo", ["...bar"]]`,
|
|
`[...foo, [...bar]]`,
|
|
)
|
|
})
|
|
|
|
it('should handle nested arrays within objects', () => {
|
|
testTransformation(
|
|
`[{ foo: ["...foo"] }]`,
|
|
`[{ foo: [...foo] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle deeply nested arrays within spread objects', () => {
|
|
testTransformation(
|
|
`[{ foo: ["...foo", { bar: ['bar', "...bar" ]}] }]`,
|
|
`[{ foo: [...foo, { bar: ['bar', ...bar ]}] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle optional paths in spread', () => {
|
|
testTransformation(
|
|
`[{ foo: ["...foo?.bar"] }]`,
|
|
`[{ foo: [...foo?.bar] }]`,
|
|
|
|
)
|
|
})
|
|
|
|
it('should handle computed property paths (\') within spread', () => {
|
|
testTransformation(
|
|
`[{ foo: ["...foo['bar']"] }]`,
|
|
`[{ foo: [...foo['bar']] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle computed property paths (") within spread', () => {
|
|
testTransformation(
|
|
`[{ foo: ['...foo["bar"]'] }]`,
|
|
`[{ foo: [...foo["bar"]] }]`,
|
|
)
|
|
})
|
|
|
|
it('should handle indexed paths in spread', () => {
|
|
testTransformation(
|
|
`[{ foo: ["...foo[0]"] }]`,
|
|
`[{ foo: [...foo[0]] }]`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('buildTailwindThemeColorsFromCssVars', () => {
|
|
it('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))',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('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))',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('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))',
|
|
},
|
|
})
|
|
})
|
|
})
|