Skip to content

Commit dd23e0f

Browse files
committed
Handle utilities with 1 keys in a JS config
1 parent 3cece1d commit dd23e0f

File tree

3 files changed

+159
-32
lines changed

3 files changed

+159
-32
lines changed

packages/tailwindcss/src/compat/apply-config-to-theme.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { describe, expect, test } from 'vitest'
22
import { buildDesignSystem } from '../design-system'
33
import { Theme, ThemeOptions } from '../theme'
4-
import { applyConfigToTheme, keyPathToCssProperty } from './apply-config-to-theme'
4+
import {
5+
applyConfigToTheme,
6+
keyPathToCssProperty,
7+
keyPathsToCssProperty,
8+
} from './apply-config-to-theme'
59
import { resolveConfig } from './config/resolve-config'
610

711
test('config values can be merged into the theme', () => {
@@ -213,6 +217,48 @@ describe('keyPathToCssProperty', () => {
213217
})
214218
})
215219

220+
describe('keyPathsToCssProperty', () => {
221+
test.each([
222+
// No "1" entries - should return single result
223+
[['width', '40', '2/5'], ['width-40-2/5']],
224+
[['spacing', '0.5'], ['spacing-0_5']],
225+
226+
// Single "1" entry before the end - should return two variants
227+
[
228+
['fontSize', 'xs', '1', 'lineHeight'],
229+
['text-xs--line-height', 'text-xs-1-line-height'],
230+
],
231+
232+
// Multiple "1" entries before the end - should only split the last "1"
233+
[
234+
['test', '1', 'middle', '1', 'end'],
235+
['test-1-middle--end', 'test-1-middle-1-end'],
236+
],
237+
238+
// A "1" at the end means everything should be kept as-is
239+
[['spacing', '1'], ['spacing-1']],
240+
241+
// Even when there are other 1s in the path
242+
[['test', '1', 'middle', '1'], ['test-1-middle-1']],
243+
244+
[['colors', 'a', '1', 'DEFAULT'], ['color-a-1']],
245+
[
246+
['colors', 'a', '1', 'foo'],
247+
['color-a--foo', 'color-a-1-foo'],
248+
],
249+
])('converts %s to %s', (keyPath, expected) => {
250+
expect(keyPathsToCssProperty(keyPath)).toEqual(expected)
251+
})
252+
253+
test('returns empty array for container path', () => {
254+
expect(keyPathsToCssProperty(['container', 'sm'])).toEqual([])
255+
})
256+
257+
test('returns empty array for invalid keys', () => {
258+
expect(keyPathsToCssProperty(['test', 'invalid@key'])).toEqual([])
259+
})
260+
})
261+
216262
test('converts opacity modifiers from decimal to percentage values', () => {
217263
let theme = new Theme()
218264
let design = buildDesignSystem(theme)

packages/tailwindcss/src/compat/apply-config-to-theme.ts

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ export function applyConfigToTheme(
2525
replacedThemeKeys: Set<string>,
2626
) {
2727
for (let replacedThemeKey of replacedThemeKeys) {
28-
let name = keyPathToCssProperty([replacedThemeKey])
29-
if (!name) continue
30-
31-
designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)
28+
for (let name of keyPathsToCssProperty([replacedThemeKey])) {
29+
designSystem.theme.clearNamespace(`--${name}`, ThemeOptions.DEFAULT)
30+
}
3231
}
3332

3433
for (let [path, value] of themeableValues(theme)) {
@@ -50,14 +49,13 @@ export function applyConfigToTheme(
5049
}
5150
}
5251

53-
let name = keyPathToCssProperty(path)
54-
if (!name) continue
55-
56-
designSystem.theme.add(
57-
`--${name}`,
58-
'' + value,
59-
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
60-
)
52+
for (let name of keyPathsToCssProperty(path)) {
53+
designSystem.theme.add(
54+
`--${name}`,
55+
'' + value,
56+
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
57+
)
58+
}
6159
}
6260

6361
// If someone has updated `fontFamily.sans` or `fontFamily.mono` in a JS
@@ -150,8 +148,12 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk
150148
const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/
151149

152150
export function keyPathToCssProperty(path: string[]) {
151+
return keyPathsToCssProperty(path)[0] ?? null
152+
}
153+
154+
export function keyPathsToCssProperty(path: string[]): string[] {
153155
// The legacy container component config should not be included in the Theme
154-
if (path[0] === 'container') return null
156+
if (path[0] === 'container') return []
155157

156158
path = path.slice()
157159

@@ -170,32 +172,43 @@ export function keyPathToCssProperty(path: string[]) {
170172
if (path[0] === 'transitionTimingFunction') path[0] = 'ease'
171173

172174
for (let part of path) {
173-
if (!IS_VALID_KEY.test(part)) return null
175+
if (!IS_VALID_KEY.test(part)) return []
174176
}
175177

176-
return (
177-
path
178-
// [1] should move into the nested object tuple. To create the CSS variable
179-
// name for this, we replace it with an empty string that will result in two
180-
// subsequent dashes when joined.
181-
//
182-
// E.g.:
183-
// - `fontSize.xs.1.lineHeight` -> `font-size-xs--line-height`
184-
// - `spacing.1` -> `--spacing-1`
185-
.map((path, idx, all) => (path === '1' && idx !== all.length - 1 ? '' : path))
186-
187-
// Resolve the key path to a CSS variable segment
178+
// Find the position of the last `1` as long as it's not at the end
179+
let lastOnePosition = path.lastIndexOf('1')
180+
if (lastOnePosition === path.length - 1) lastOnePosition = -1
181+
182+
// Generate two combinations based on tuple access:
183+
let paths: string[][] = []
184+
185+
// Option 1: Replace the last "1" with empty string if it exists
186+
//
187+
// We place this first as we "prefer" treating this as a tuple access. The exception to this is if
188+
// the keypath ends in `DEFAULT` otherwise we'd see a key that ends in a dash like `--color-a-`
189+
if (lastOnePosition !== -1 && path.at(-1) !== 'DEFAULT') {
190+
let modified = path.slice()
191+
modified[lastOnePosition] = ''
192+
paths.push(modified)
193+
}
194+
195+
// Option 2: The path as is
196+
paths.push(path)
197+
198+
return paths.map((path) => {
199+
// Remove the `DEFAULT` key at the end of a path
200+
// We're reading from CSS anyway so it'll be a string
201+
if (path.at(-1) === 'DEFAULT') path = path.slice(0, -1)
202+
203+
// Resolve the key path to a CSS variable segment
204+
return path
188205
.map((part) =>
189206
part
190207
.replaceAll('.', '_')
191208
.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`),
192209
)
193-
194-
// Remove the `DEFAULT` key at the end of a path
195-
// We're reading from CSS anyway so it'll be a string
196-
.filter((part, index) => part !== 'DEFAULT' || index !== path.length - 1)
197210
.join('-')
198-
)
211+
})
199212
}
200213

201214
function isValidThemePrimitive(value: unknown) {

packages/tailwindcss/src/compat/config.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,74 @@ test('Variants in CSS overwrite variants from plugins', async () => {
297297
`)
298298
})
299299

300+
test('A `1` key is treated like a nested theme key *and* a normal theme key', async () => {
301+
let input = css`
302+
@tailwind utilities;
303+
@config "./config.js";
304+
`
305+
306+
let compiler = await compile(input, {
307+
loadModule: async () => ({
308+
module: {
309+
theme: {
310+
fontSize: {
311+
xs: ['0.5rem', { lineHeight: '0.25rem' }],
312+
},
313+
colors: {
314+
a: {
315+
1: { DEFAULT: '#ffffff', hovered: '#000000' },
316+
2: { DEFAULT: '#000000', hovered: '#c0ffee' },
317+
},
318+
b: {
319+
1: '#ffffff',
320+
2: '#000000',
321+
},
322+
},
323+
},
324+
},
325+
base: '/root',
326+
path: '',
327+
}),
328+
})
329+
330+
expect(
331+
compiler.build([
332+
'text-xs',
333+
334+
'text-a-1',
335+
'text-a-2',
336+
'text-a-1-hovered',
337+
'text-a-2-hovered',
338+
'text-b-1',
339+
'text-b-2',
340+
]),
341+
).toMatchInlineSnapshot(`
342+
".text-xs {
343+
font-size: 0.5rem;
344+
line-height: var(--tw-leading, 0.25rem);
345+
}
346+
.text-a-1 {
347+
color: #ffffff;
348+
}
349+
.text-a-1-hovered {
350+
color: #000000;
351+
}
352+
.text-a-2 {
353+
color: #000000;
354+
}
355+
.text-a-2-hovered {
356+
color: #c0ffee;
357+
}
358+
.text-b-1 {
359+
color: #ffffff;
360+
}
361+
.text-b-2 {
362+
color: #000000;
363+
}
364+
"
365+
`)
366+
})
367+
300368
describe('theme callbacks', () => {
301369
test('tuple values from the config overwrite `@theme default` tuple-ish values from the CSS theme', async ({
302370
expect,

0 commit comments

Comments
 (0)