Skip to content

Commit 500dbe2

Browse files
bcorboldmatsuyoshi30shadcn
authored
fix(shadcn) arrays and nested deeply nested spread (#5711)
* 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 <sfbgwm30@gmail.com> Co-authored-by: shadcn <m@shadcn.com>
1 parent c577ee0 commit 500dbe2

4 files changed

Lines changed: 562 additions & 20 deletions

File tree

.changeset/empty-pants-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": patch
3+
---
4+
5+
Update spread/unspread helpers to handle ArrayLiteralExpression and nested values within arrays

packages/shadcn/src/utils/updaters/update-tailwind-config.ts

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import deepmerge from "deepmerge"
99
import objectToString from "stringify-object"
1010
import { type Config as TailwindConfig } from "tailwindcss"
1111
import {
12+
ArrayLiteralExpression,
1213
ObjectLiteralExpression,
1314
Project,
1415
PropertyAssignment,
@@ -194,8 +195,11 @@ async function addTailwindConfigTheme(
194195
if (themeInitializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
195196
const themeObjectString = themeInitializer.getText()
196197
const themeObject = await parseObjectLiteral(themeObjectString)
197-
const result = deepmerge(themeObject, theme)
198+
const result = deepmerge(themeObject, theme, {
199+
arrayMerge: (dst, src) => src,
200+
})
198201
const resultString = objectToString(result)
202+
.replace(/\'\.\.\.(.*)\'/g, "...$1") // Remove quote around spread element
199203
.replace(/\'\"/g, "'") // Replace `\" with "
200204
.replace(/\"\'/g, "'") // Replace `\" with "
201205
.replace(/\'\[/g, "[") // Replace `[ with [
@@ -287,7 +291,8 @@ export function nestSpreadProperties(obj: ObjectLiteralExpression) {
287291

288292
// Replace spread with a property assignment
289293
obj.insertPropertyAssignment(i, {
290-
name: `___${spreadText.replace(/^\.\.\./, "")}`,
294+
// Need to escape the name with " so that deepmerge doesn't mishandle the key
295+
name: `"___${spreadText.replace(/^\.\.\./, "")}"`,
291296
initializer: `"...${spreadText.replace(/^\.\.\./, "")}"`,
292297
})
293298

@@ -305,11 +310,41 @@ export function nestSpreadProperties(obj: ObjectLiteralExpression) {
305310
nestSpreadProperties(
306311
initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
307312
)
313+
} else if (
314+
initializer &&
315+
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
316+
) {
317+
nestSpreadElements(
318+
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
319+
)
308320
}
309321
}
310322
}
311323
}
312324

325+
export function nestSpreadElements(arr: ArrayLiteralExpression) {
326+
const elements = arr.getElements()
327+
for (let j = 0; j < elements.length; j++) {
328+
const element = elements[j]
329+
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) {
330+
// Recursive check on objects within arrays
331+
nestSpreadProperties(
332+
element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
333+
)
334+
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
335+
// Recursive check on nested arrays
336+
nestSpreadElements(
337+
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
338+
)
339+
} else if (element.isKind(SyntaxKind.SpreadElement)) {
340+
const spreadText = element.getText()
341+
// Spread element within an array
342+
arr.removeElement(j)
343+
arr.insertElement(j, `"${spreadText}"`)
344+
}
345+
}
346+
}
347+
313348
export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
314349
const properties = obj.getProperties()
315350

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

322-
if (initializer?.isKind(SyntaxKind.StringLiteral)) {
323-
const value = initializer.getLiteralValue()
357+
if (initializer && initializer.isKind(SyntaxKind.StringLiteral)) {
358+
const value = initializer
359+
.asKindOrThrow(SyntaxKind.StringLiteral)
360+
.getLiteralValue()
324361
if (value.startsWith("...")) {
325362
obj.insertSpreadAssignment(i, { expression: value.slice(3) })
326363
propAssignment.remove()
327364
}
328365
} else if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
329366
unnestSpreadProperties(initializer as ObjectLiteralExpression)
367+
} else if (
368+
initializer &&
369+
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
370+
) {
371+
unnsetSpreadElements(
372+
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
373+
)
374+
}
375+
}
376+
}
377+
}
378+
379+
export function unnsetSpreadElements(arr: ArrayLiteralExpression) {
380+
const elements = arr.getElements()
381+
for (let j = 0; j < elements.length; j++) {
382+
const element = elements[j]
383+
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) {
384+
// Recursive check on objects within arrays
385+
unnestSpreadProperties(
386+
element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
387+
)
388+
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
389+
// Recursive check on nested arrays
390+
unnsetSpreadElements(
391+
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
392+
)
393+
} else if (element.isKind(SyntaxKind.StringLiteral)) {
394+
const spreadText = element.getText()
395+
// check if spread element
396+
const spreadTest = /(?:^['"])(\.\.\..*)(?:['"]$)/g
397+
if (spreadTest.test(spreadText)) {
398+
arr.removeElement(j)
399+
arr.insertElement(j, spreadText.replace(spreadTest, "$1"))
330400
}
331401
}
332402
}
@@ -363,6 +433,12 @@ function parseObjectLiteralExpression(node: ObjectLiteralExpression): any {
363433
result[name] = parseObjectLiteralExpression(
364434
property.getInitializer() as ObjectLiteralExpression
365435
)
436+
} else if (
437+
property.getInitializer()?.isKind(SyntaxKind.ArrayLiteralExpression)
438+
) {
439+
result[name] = parseArrayLiteralExpression(
440+
property.getInitializer() as ArrayLiteralExpression
441+
)
366442
} else {
367443
result[name] = parseValue(property.getInitializer())
368444
}
@@ -371,20 +447,44 @@ function parseObjectLiteralExpression(node: ObjectLiteralExpression): any {
371447
return result
372448
}
373449

450+
function parseArrayLiteralExpression(node: ArrayLiteralExpression): any[] {
451+
const result: any[] = []
452+
for (const element of node.getElements()) {
453+
if (element.isKind(SyntaxKind.ObjectLiteralExpression)) {
454+
result.push(
455+
parseObjectLiteralExpression(
456+
element.asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
457+
)
458+
)
459+
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
460+
result.push(
461+
parseArrayLiteralExpression(
462+
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
463+
)
464+
)
465+
} else {
466+
result.push(parseValue(element))
467+
}
468+
}
469+
return result
470+
}
471+
374472
function parseValue(node: any): any {
375-
switch (node.kind) {
473+
switch (node.getKind()) {
376474
case SyntaxKind.StringLiteral:
377-
return node.text
475+
return node.getText()
378476
case SyntaxKind.NumericLiteral:
379-
return Number(node.text)
477+
return Number(node.getText())
380478
case SyntaxKind.TrueKeyword:
381479
return true
382480
case SyntaxKind.FalseKeyword:
383481
return false
384482
case SyntaxKind.NullKeyword:
385483
return null
386484
case SyntaxKind.ArrayLiteralExpression:
387-
return node.elements.map(parseValue)
485+
return node.getElements().map(parseValue)
486+
case SyntaxKind.ObjectLiteralExpression:
487+
return parseObjectLiteralExpression(node)
388488
default:
389489
return node.getText()
390490
}

packages/shadcn/test/utils/updaters/__snapshots__/update-tailwind-config.test.ts.snap

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,14 @@ const config: Config = {
324324
theme: {
325325
extend: {
326326
fontFamily: {
327-
sans: ["var(--font-geist-sans)", ...fontFamily.sans],
328-
mono: ["var(--font-mono)", ...fontFamily.mono],
327+
sans: [
328+
'var(--font-geist-sans)',
329+
...fontFamily.sans
330+
],
331+
mono: [
332+
'var(--font-mono)',
333+
...fontFamily.mono
334+
],
329335
heading: [
330336
'var(--font-geist-sans)'
331337
]
@@ -369,6 +375,75 @@ export default config
369375
"
370376
`;
371377

378+
exports[`transformTailwindConfig -> theme > should handle objects nested in arrays 1`] = `
379+
"import type { Config } from 'tailwindcss'
380+
381+
const config: Config = {
382+
darkMode: ["class"],
383+
content: [
384+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
385+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
386+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
387+
],
388+
theme: {
389+
extend: {
390+
fontSize: {
391+
xs: [
392+
'0.75rem',
393+
{
394+
lineHeight: '1rem'
395+
}
396+
],
397+
sm: [
398+
'0.875rem',
399+
{
400+
lineHeight: '1.25rem'
401+
}
402+
],
403+
xl: [
404+
'clamp(1.5rem, 1.04vi + 1.17rem, 2rem)',
405+
{
406+
lineHeight: '1.2',
407+
letterSpacing: '-0.02em',
408+
fontWeight: '600'
409+
}
410+
]
411+
}
412+
}
413+
},
414+
}
415+
export default config
416+
"
417+
`;
418+
419+
exports[`transformTailwindConfig -> theme > should keep arrays when formatted on multilines 1`] = `
420+
"import type { Config } from 'tailwindcss'
421+
422+
const config: Config = {
423+
darkMode: ["class"],
424+
content: [
425+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
426+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
427+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
428+
],
429+
theme: {
430+
extend: {
431+
fontFamily: {
432+
sans: [
433+
'Figtree',
434+
...defaultTheme.fontFamily.sans
435+
],
436+
mono: [
437+
'Foo'
438+
]
439+
}
440+
}
441+
},
442+
}
443+
export default config
444+
"
445+
`;
446+
372447
exports[`transformTailwindConfig -> theme > should keep quotes in strings 1`] = `
373448
"import type { Config } from 'tailwindcss'
374449
@@ -382,7 +457,10 @@ const config: Config = {
382457
theme: {
383458
extend: {
384459
fontFamily: {
385-
sans: ['Figtree', ...defaultTheme.fontFamily.sans]
460+
sans: [
461+
'Figtree',
462+
...defaultTheme.fontFamily.sans
463+
]
386464
},
387465
colors: {
388466
...defaultColors,
@@ -448,6 +526,12 @@ const config: Config = {
448526
],
449527
theme: {
450528
extend: {
529+
fontFamily: {
530+
sans: [
531+
'ui-sans-serif',
532+
'sans-serif'
533+
]
534+
},
451535
colors: {
452536
background: 'hsl(var(--background))',
453537
foreground: 'hsl(var(--foreground))',
@@ -466,3 +550,23 @@ const config: Config = {
466550
export default config
467551
"
468552
`;
553+
554+
exports[`transformTailwindConfig -> theme > should preserve boolean values 1`] = `
555+
"import type { Config } from 'tailwindcss'
556+
557+
const config: Config = {
558+
darkMode: ["class"],
559+
content: [
560+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
561+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
562+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
563+
],
564+
theme: {
565+
container: {
566+
center: true
567+
}
568+
},
569+
}
570+
export default config
571+
"
572+
`;

0 commit comments

Comments
 (0)