From 530774b186f67f85f0cb96afa3ec6a5b5f841b26 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Wed, 4 Sep 2024 15:30:21 -0400 Subject: [PATCH] Ensure `--default-font-*` and `--default-mono-font-*` variables respect theme customizations in JS config files (#14344) This PR fixes an issue where variables like `--default-font-family` wouldn't behave as expected when customizing `fontFamily.sans` or `fontFamily.mono` in a JS config. Because theme values added by JS config files are added as `reference`, customizing `fontFamily.sans` means the `--font-family-sans` variable no longer exists in the generated CSS. The `--default-font-family` variable is set to `var(--font-family-sans)` by default, so because that variable doesn't exist, `--default-font-family` is effectively undefined and the browser default font stack is used. This is unexpected because historically customizing `fontFamily.sans` has updated your default font for your entire project. --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com> --- CHANGELOG.md | 1 + .../src/compat/apply-config-to-theme.ts | 68 ++- .../tailwindcss/src/compat/config.test.ts | 484 +++++++++++++++++- packages/tailwindcss/src/theme.ts | 4 + 4 files changed, 555 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ddae217..5bee24e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don’t suggest named opacity modifiers in intellisense ([#14339](https://github.com/tailwindlabs/tailwindcss/pull/14339)) - Fix a crash with older Node.js versions ([#14342](https://github.com/tailwindlabs/tailwindcss/pull/14342)) - Support defining theme values as arrays of strings in JS config files ([#14343](https://github.com/tailwindlabs/tailwindcss/pull/14343)) +- Ensure `--default-font-*` and `--default-mono-font-*` variables respect theme customizations in JS config files ([#14344](https://github.com/tailwindlabs/tailwindcss/pull/14344)) ## [4.0.0-alpha.21] - 2024-09-02 diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index b3a24193e..cac751e53 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -2,12 +2,28 @@ import type { DesignSystem } from '../design-system' import { resolveConfig, type ConfigFile } from './config/resolve-config' import type { ResolvedConfig } from './config/types' +function resolveThemeValue(value: unknown, subValue: string | null = null): string | null { + if ( + Array.isArray(value) && + value.length === 2 && + typeof value[1] === 'object' && + typeof value[1] !== null + ) { + return subValue ? (value[1][subValue] ?? null) : value[0] + } else if (Array.isArray(value) && subValue === null) { + return value.join(', ') + } else if (typeof value === 'string' && subValue === null) { + return value + } + + return null +} + export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFile[]) { let theme = resolveConfig(designSystem, configs).theme for (let [path, value] of themeableValues(theme)) { let name = keyPathToCssProperty(path) - designSystem.theme.add(`--${name}`, value as any, { isInline: true, isReference: true, @@ -15,6 +31,56 @@ export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFi }) } + // If someone has updated `fontFamily.sans` or `fontFamily.mono` in a JS + // config, we need to make sure variables like `--default-font-family` and + // `--default-font-feature-settings` are updated to match those explicit + // values, because variables like `--font-family-sans` and + // `--font-family-sans--feature-settings` (which the `--default-font-*` + // variables reference) won't exist in the generated CSS. + if (Object.hasOwn(theme, 'fontFamily')) { + let options = { + isInline: true, + isReference: false, + isDefault: true, + } + + // Replace `--default-font-*` with `fontFamily.sans` values + { + let fontFamily = resolveThemeValue(theme.fontFamily.sans) + if (fontFamily && designSystem.theme.hasDefault('--font-family-sans')) { + designSystem.theme.add('--default-font-family', fontFamily, options) + designSystem.theme.add( + '--default-font-feature-settings', + resolveThemeValue(theme.fontFamily.sans, 'fontFeatureSettings') ?? 'normal', + options, + ) + designSystem.theme.add( + '--default-font-variation-settings', + resolveThemeValue(theme.fontFamily.sans, 'fontVariationSettings') ?? 'normal', + options, + ) + } + } + + // Replace `--default-mono-font-*` with `fontFamily.mono` values + { + let fontFamily = resolveThemeValue(theme.fontFamily.mono) + if (fontFamily && designSystem.theme.hasDefault('--font-family-mono')) { + designSystem.theme.add('--default-mono-font-family', 'theme(fontFamily.mono)', options) + designSystem.theme.add( + '--default-mono-font-feature-settings', + resolveThemeValue(theme.fontFamily.mono, 'fontFeatureSettings') ?? 'normal', + options, + ) + designSystem.theme.add( + '--default-mono-font-variation-settings', + resolveThemeValue(theme.fontFamily.mono, 'fontVariationSettings') ?? 'normal', + options, + ) + } + } + } + return theme } diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 23d8aedd9..72278a60b 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,4 +1,4 @@ -import { test } from 'vitest' +import { describe, test } from 'vitest' import { compile } from '..' import plugin from '../plugin' @@ -229,3 +229,485 @@ test('Variants in CSS overwrite variants from plugins', async ({ expect }) => { " `) }) + +describe('default font family compatibility', () => { + test('overriding `fontFamily.sans` sets `--default-font-family`', async ({ expect }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: 'Potato Sans', + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: normal; + --default-font-variation-settings: normal; + } + .font-sans { + font-family: Potato Sans; + } + " + `) + }) + + test('overriding `fontFamily.sans[1].fontFeatureSettings` sets `--default-font-feature-settings`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: "cv06"; + --default-font-variation-settings: normal; + } + .font-sans { + font-family: Potato Sans; + font-feature-settings: "cv06"; + } + " + `) + }) + + test('overriding `fontFamily.sans[1].fontVariationSettings` sets `--default-font-variation-settings`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: normal; + --default-font-variation-settings: "XHGT" 0.7; + } + .font-sans { + font-family: Potato Sans; + font-variation-settings: "XHGT" 0.7; + } + " + `) + }) + + test('overriding `fontFeatureSettings` and `fontVariationSettings` for `fontFamily.sans` sets `--default-font-feature-settings` and `--default-font-variation-settings`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: [ + 'Potato Sans', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Potato Sans; + --default-font-feature-settings: "cv06"; + --default-font-variation-settings: "XHGT" 0.7; + } + .font-sans { + font-family: Potato Sans; + font-feature-settings: "cv06"; + font-variation-settings: "XHGT" 0.7; + } + " + `) + }) + + test('overriding `--font-family-sans` in `@theme` without `default` preserves the original `--default-font-*` values', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @theme { + --font-family-sans: Sandwich Sans; + } + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: 'Potato Sans', + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + --font-family-sans: Sandwich Sans; + } + .font-sans { + font-family: var(--font-family-sans, Sandwich Sans); + } + " + `) + }) + + test('overriding `fontFamily.sans` in a config file with an array sets `--default-font-family`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: Inter, system-ui, sans-serif; + --default-font-feature-settings: normal; + --default-font-variation-settings: normal; + } + .font-sans { + font-family: Inter, system-ui, sans-serif; + } + " + `) + }) + + test('overriding `fontFamily.sans` in a config file with an unexpected type is ignored', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + sans: { foo: 'bar', banana: 'sandwich' }, + }, + }, + }), + }) + + expect(compiler.build(['font-sans'])).toMatchInlineSnapshot(` + ":root { + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + } + " + `) + }) + + test('overriding `fontFamily.mono` sets `--default-mono-font-family`', async ({ expect }) => { + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + mono: 'Potato Mono', + }, + }, + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: normal; + --default-mono-font-variation-settings: normal; + } + .font-mono { + font-family: Potato Mono; + } + " + `) + }) + + test('overriding `fontFamily.mono[1].fontFeatureSettings` sets `--default-mono-font-feature-settings`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], + }, + }, + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: "cv06"; + --default-mono-font-variation-settings: normal; + } + .font-mono { + font-family: Potato Mono; + font-feature-settings: "cv06"; + } + " + `) + }) + + test('overriding `fontFamily.mono[1].fontVariationSettings` sets `--default-mono-font-variation-settings`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], + }, + }, + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: normal; + --default-mono-font-variation-settings: "XHGT" 0.7; + } + .font-mono { + font-family: Potato Mono; + font-variation-settings: "XHGT" 0.7; + } + " + `) + }) + + test('overriding `fontFeatureSettings` and `fontVariationSettings` for `fontFamily.mono` sets `--default-mono-font-feature-settings` and `--default-mono-font-variation-settings`', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + mono: [ + 'Potato Mono', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], + }, + }, + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: Potato Mono; + --default-mono-font-feature-settings: "cv06"; + --default-mono-font-variation-settings: "XHGT" 0.7; + } + .font-mono { + font-family: Potato Mono; + font-feature-settings: "cv06"; + font-variation-settings: "XHGT" 0.7; + } + " + `) + }) + + test('overriding `--font-family-mono` in `@theme` without `default` preserves the original `--default-mono-font-*` values', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @theme { + --font-family-mono: Sandwich Mono; + } + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + mono: 'Potato Mono', + }, + }, + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + --font-family-mono: Sandwich Mono; + } + .font-mono { + font-family: var(--font-family-mono, Sandwich Mono); + } + " + `) + }) + + test('overriding `fontFamily.mono` in a config file with an unexpected type is ignored', async ({ + expect, + }) => { + let input = css` + @theme default { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + @config "./config.js"; + @tailwind utilities; + ` + + let compiler = await compile(input, { + loadConfig: async () => ({ + theme: { + fontFamily: { + mono: { foo: 'bar', banana: 'sandwich' }, + }, + }, + }), + }) + + expect(compiler.build(['font-mono'])).toMatchInlineSnapshot(` + ":root { + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + } + " + `) + }) +}) diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index 2a7bdb80e..4a8f79b4e 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -67,6 +67,10 @@ export class Theme { return null } + hasDefault(key: string): boolean { + return this.values.get(key)?.isDefault ?? false + } + entries() { return this.values.entries() }