Skip to content

Commit

Permalink
fix(shadcn) arrays and nested deeply nested spread (shadcn-ui#5711)
Browse files Browse the repository at this point in the history
* fix: tailwind config updater parser

* fix: remove quote around spread element

* fix: specify deepmerge option for array

* fix(shadcn): Nested and spread array elements

* add test case for boolean primitive

---------

Co-authored-by: matsuyoshi30 <[email protected]>
Co-authored-by: shadcn <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2024
1 parent 82e862e commit bdf00c8
Show file tree
Hide file tree
Showing 4 changed files with 562 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-pants-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn": patch
---

Update spread/unspread helpers to handle ArrayLiteralExpression and nested values within arrays
116 changes: 108 additions & 8 deletions packages/shadcn/src/utils/updaters/update-tailwind-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import deepmerge from "deepmerge"
import objectToString from "stringify-object"
import { type Config as TailwindConfig } from "tailwindcss"
import {
ArrayLiteralExpression,
ObjectLiteralExpression,
Project,
PropertyAssignment,
Expand Down Expand Up @@ -194,8 +195,11 @@ async function addTailwindConfigTheme(
if (themeInitializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
const themeObjectString = themeInitializer.getText()
const themeObject = await parseObjectLiteral(themeObjectString)
const result = deepmerge(themeObject, theme)
const result = deepmerge(themeObject, theme, {
arrayMerge: (dst, src) => src,
})
const resultString = objectToString(result)
.replace(/\'\.\.\.(.*)\'/g, "...$1") // Remove quote around spread element
.replace(/\'\"/g, "'") // Replace `\" with "
.replace(/\"\'/g, "'") // Replace `\" with "
.replace(/\'\[/g, "[") // Replace `[ with [
Expand Down Expand Up @@ -287,7 +291,8 @@ export function nestSpreadProperties(obj: ObjectLiteralExpression) {

// Replace spread with a property assignment
obj.insertPropertyAssignment(i, {
name: `___${spreadText.replace(/^\.\.\./, "")}`,
// Need to escape the name with " so that deepmerge doesn't mishandle the key
name: `"___${spreadText.replace(/^\.\.\./, "")}"`,
initializer: `"...${spreadText.replace(/^\.\.\./, "")}"`,
})

Expand All @@ -305,11 +310,41 @@ export function nestSpreadProperties(obj: ObjectLiteralExpression) {
nestSpreadProperties(
initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
)
} else if (
initializer &&
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
) {
nestSpreadElements(
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
}
}
}
}

export function nestSpreadElements(arr: ArrayLiteralExpression) {
const elements = arr.getElements()
for (let j = 0; j < elements.length; j++) {
const element = elements[j]
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) {
// Recursive check on objects within arrays
nestSpreadProperties(
element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
)
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
// Recursive check on nested arrays
nestSpreadElements(
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
} else if (element.isKind(SyntaxKind.SpreadElement)) {
const spreadText = element.getText()
// Spread element within an array
arr.removeElement(j)
arr.insertElement(j, `"${spreadText}"`)
}
}
}

export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
const properties = obj.getProperties()

Expand All @@ -319,14 +354,49 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
const propAssignment = prop as PropertyAssignment
const initializer = propAssignment.getInitializer()

if (initializer?.isKind(SyntaxKind.StringLiteral)) {
const value = initializer.getLiteralValue()
if (initializer && initializer.isKind(SyntaxKind.StringLiteral)) {
const value = initializer
.asKindOrThrow(SyntaxKind.StringLiteral)
.getLiteralValue()
if (value.startsWith("...")) {
obj.insertSpreadAssignment(i, { expression: value.slice(3) })
propAssignment.remove()
}
} else if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
unnestSpreadProperties(initializer as ObjectLiteralExpression)
} else if (
initializer &&
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
) {
unnsetSpreadElements(
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
}
}
}
}

export function unnsetSpreadElements(arr: ArrayLiteralExpression) {
const elements = arr.getElements()
for (let j = 0; j < elements.length; j++) {
const element = elements[j]
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) {
// Recursive check on objects within arrays
unnestSpreadProperties(
element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
)
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
// Recursive check on nested arrays
unnsetSpreadElements(
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
} else if (element.isKind(SyntaxKind.StringLiteral)) {
const spreadText = element.getText()
// check if spread element
const spreadTest = /(?:^['"])(\.\.\..*)(?:['"]$)/g
if (spreadTest.test(spreadText)) {
arr.removeElement(j)
arr.insertElement(j, spreadText.replace(spreadTest, "$1"))
}
}
}
Expand Down Expand Up @@ -363,6 +433,12 @@ function parseObjectLiteralExpression(node: ObjectLiteralExpression): any {
result[name] = parseObjectLiteralExpression(
property.getInitializer() as ObjectLiteralExpression
)
} else if (
property.getInitializer()?.isKind(SyntaxKind.ArrayLiteralExpression)
) {
result[name] = parseArrayLiteralExpression(
property.getInitializer() as ArrayLiteralExpression
)
} else {
result[name] = parseValue(property.getInitializer())
}
Expand All @@ -371,20 +447,44 @@ function parseObjectLiteralExpression(node: ObjectLiteralExpression): any {
return result
}

function parseArrayLiteralExpression(node: ArrayLiteralExpression): any[] {
const result: any[] = []
for (const element of node.getElements()) {
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) {
result.push(
parseObjectLiteralExpression(
element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
)
)
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
result.push(
parseArrayLiteralExpression(
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
)
} else {
result.push(parseValue(element))
}
}
return result
}

function parseValue(node: any): any {
switch (node.kind) {
switch (node.getKind()) {
case SyntaxKind.StringLiteral:
return node.text
return node.getText()
case SyntaxKind.NumericLiteral:
return Number(node.text)
return Number(node.getText())
case SyntaxKind.TrueKeyword:
return true
case SyntaxKind.FalseKeyword:
return false
case SyntaxKind.NullKeyword:
return null
case SyntaxKind.ArrayLiteralExpression:
return node.elements.map(parseValue)
return node.getElements().map(parseValue)
case SyntaxKind.ObjectLiteralExpression:
return parseObjectLiteralExpression(node)
default:
return node.getText()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,14 @@ const config: Config = {
theme: {
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans],
mono: ["var(--font-mono)", ...fontFamily.mono],
sans: [
'var(--font-geist-sans)',
...fontFamily.sans
],
mono: [
'var(--font-mono)',
...fontFamily.mono
],
heading: [
'var(--font-geist-sans)'
]
Expand Down Expand Up @@ -369,6 +375,75 @@ export default config
"
`;

exports[`transformTailwindConfig -> theme > should handle objects nested in arrays 1`] = `
"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: {
fontSize: {
xs: [
'0.75rem',
{
lineHeight: '1rem'
}
],
sm: [
'0.875rem',
{
lineHeight: '1.25rem'
}
],
xl: [
'clamp(1.5rem, 1.04vi + 1.17rem, 2rem)',
{
lineHeight: '1.2',
letterSpacing: '-0.02em',
fontWeight: '600'
}
]
}
}
},
}
export default config
"
`;

exports[`transformTailwindConfig -> theme > should keep arrays when formatted on multilines 1`] = `
"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: {
fontFamily: {
sans: [
'Figtree',
...defaultTheme.fontFamily.sans
],
mono: [
'Foo'
]
}
}
},
}
export default config
"
`;

exports[`transformTailwindConfig -> theme > should keep quotes in strings 1`] = `
"import type { Config } from 'tailwindcss'
Expand All @@ -382,7 +457,10 @@ const config: Config = {
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans]
sans: [
'Figtree',
...defaultTheme.fontFamily.sans
]
},
colors: {
...defaultColors,
Expand Down Expand Up @@ -448,6 +526,12 @@ const config: Config = {
],
theme: {
extend: {
fontFamily: {
sans: [
'ui-sans-serif',
'sans-serif'
]
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
Expand All @@ -466,3 +550,23 @@ const config: Config = {
export default config
"
`;

exports[`transformTailwindConfig -> theme > should preserve boolean values 1`] = `
"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: {
container: {
center: true
}
},
}
export default config
"
`;
Loading

0 comments on commit bdf00c8

Please sign in to comment.