diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts index 9d8b176bd858..c1b9669ac466 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts @@ -1,4 +1,5 @@ import { cloneCandidate } from '../../../../tailwindcss/src/candidate' +import { createSignatureOptions } from '../../../../tailwindcss/src/canonicalize-candidates' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { @@ -14,8 +15,9 @@ export function migrateArbitraryVariants( _userConfig: Config | null, rawCandidate: string, ): string { - let signatures = computeVariantSignature.get(designSystem) - let variants = preComputedVariants.get(designSystem) + let signatureOptions = createSignatureOptions(designSystem) + let signatures = computeVariantSignature.get(signatureOptions) + let variants = preComputedVariants.get(signatureOptions) for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { // We are only interested in the variants diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts index 9f26f0188606..b1887ae0bd7e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.test.ts @@ -17,7 +17,7 @@ function migrate(designSystem: DesignSystem, userConfig: UserConfig | null, rawC migratePrefix, migrateModernizeArbitraryValues, migrateArbitraryVariants, - (designSystem: DesignSystem, _, rawCandidate: string) => { + (designSystem: DesignSystem, _: UserConfig | null, rawCandidate: string) => { return designSystem.canonicalizeCandidates([rawCandidate]).pop() ?? rawCandidate }, ]) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts index d402847fc0e9..6060d8020cd1 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts @@ -1,5 +1,6 @@ import fs from 'node:fs/promises' import path, { extname } from 'node:path' +import { createSignatureOptions } from '../../../../tailwindcss/src/canonicalize-candidates' import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { computeUtilitySignature } from '../../../../tailwindcss/src/signatures' @@ -39,11 +40,10 @@ export const DEFAULT_MIGRATIONS: Migration[] = [ migrateModernizeArbitraryValues, ] -let migrateCached = new DefaultMap< - DesignSystem, - DefaultMap>> ->((designSystem) => { - return new DefaultMap((userConfig) => { +let migrateCached = new DefaultMap((designSystem: DesignSystem) => { + let options = createSignatureOptions(designSystem) + + return new DefaultMap((userConfig: Config | null) => { return new DefaultMap(async (rawCandidate) => { let original = rawCandidate @@ -57,7 +57,7 @@ let migrateCached = new DefaultMap< // Verify that the candidate actually makes sense at all. E.g.: `duration` // is not a valid candidate, but it will parse because `duration-` // exists. - let signature = computeUtilitySignature.get(designSystem).get(rawCandidate) + let signature = computeUtilitySignature.get(options).get(rawCandidate) if (typeof signature !== 'string') return original return rawCandidate diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index b1ba62677610..75c0d45d182e 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -2,6 +2,7 @@ import fs from 'node:fs' import path from 'node:path' import { describe, expect, test } from 'vitest' import { __unstable__loadDesignSystem } from '.' +import type { CanonicalizeOptions } from './intellisense' import { DefaultMap } from './utils/default-map' const css = String.raw @@ -63,7 +64,12 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', return candidate } - async function expectCanonicalization(input: string, candidate: string, expected: string) { + async function expectCanonicalization( + input: string, + candidate: string, + expected: string, + options?: CanonicalizeOptions, + ) { candidate = prepare(candidate) expected = prepare(expected) @@ -72,7 +78,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', } let designSystem = await designSystems.get(__dirname).get(input) - let [actual] = designSystem.canonicalizeCandidates([candidate]) + let [actual] = designSystem.canonicalizeCandidates([candidate], options) try { expect(actual).toBe(expected) @@ -937,3 +943,22 @@ describe('theme to var', () => { ]) }) }) + +describe('options', () => { + test('normalize `rem` units to `px`', async () => { + let designSystem = await __unstable__loadDesignSystem( + css` + @tailwind utilities; + @theme { + --spacing: 0.25rem; + } + `, + { base: __dirname }, + ) + + expect(designSystem.canonicalizeCandidates(['m-[16px]'])).toEqual(['m-[16px]']) + expect(designSystem.canonicalizeCandidates(['m-[16px]'], { rem: 16 })).toEqual(['m-4']) + expect(designSystem.canonicalizeCandidates(['m-[16px]'], { rem: 64 })).toEqual(['m-1']) + expect(designSystem.canonicalizeCandidates(['m-[16px]'])).toEqual(['m-[16px]']) // Ensure options don't influence shared state + }) +}) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 791da7844c32..8df31af09091 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -16,6 +16,7 @@ import { computeVariantSignature, preComputedUtilities, preComputedVariants, + type SignatureOptions, } from './signatures' import type { Writable } from './types' import { DefaultMap } from './utils/default-map' @@ -26,19 +27,47 @@ import { segment } from './utils/segment' import { toKeyPath } from './utils/to-key-path' import * as ValueParser from './value-parser' -export function canonicalizeCandidates(ds: DesignSystem, candidates: string[]): string[] { +export interface CanonicalizeOptions { + /** + * The root font size in pixels. If provided, `rem` values will be normalized + * to `px` values. + * + * E.g.: `mt-[16px]` with `rem: 16` will become `mt-4` (assuming `--spacing: 0.25rem`). + */ + rem?: number +} + +const optionsCache = new DefaultMap((designSystem: DesignSystem) => { + return new DefaultMap((rem: number | null = null) => { + return { designSystem, rem } satisfies SignatureOptions + }) +}) + +export function createSignatureOptions( + designSystem: DesignSystem, + options?: CanonicalizeOptions, +): SignatureOptions { + return optionsCache.get(designSystem).get(options?.rem ?? null) +} + +export function canonicalizeCandidates( + designSystem: DesignSystem, + candidates: string[], + options?: CanonicalizeOptions, +): string[] { let result = new Set() - let cache = canonicalizeCandidateCache.get(ds) + let cache = canonicalizeCandidateCache.get(createSignatureOptions(designSystem, options)) for (let candidate of candidates) { result.add(cache.get(candidate)) } return Array.from(result) } -const canonicalizeCandidateCache = new DefaultMap((ds: DesignSystem) => { +const canonicalizeCandidateCache = new DefaultMap((options: SignatureOptions) => { + let ds = options.designSystem let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : '' - let variantCache = canonicalizeVariantCache.get(ds) - let utilityCache = canonicalizeUtilityCache.get(ds) + let variantCache = canonicalizeVariantCache.get(options) + let utilityCache = canonicalizeUtilityCache.get(options) return new DefaultMap((rawCandidate: string, self) => { for (let candidate of ds.parseCandidate(rawCandidate)) { @@ -95,21 +124,26 @@ const canonicalizeCandidateCache = new DefaultMap((ds: DesignSystem) => { }) }) -const VARIANT_CANONICALIZATIONS = [ +type VariantCanonicalizationFunction = ( + variant: Variant, + options: SignatureOptions, +) => Variant | Variant[] + +const VARIANT_CANONICALIZATIONS: VariantCanonicalizationFunction[] = [ themeToVarVariant, arbitraryValueToBareValueVariant, modernizeArbitraryValuesVariant, arbitraryVariants, ] -const canonicalizeVariantCache = new DefaultMap((ds: DesignSystem) => { +const canonicalizeVariantCache = new DefaultMap((options: SignatureOptions) => { return new DefaultMap((variant: Variant): Variant[] => { let replacement = [variant] for (let fn of VARIANT_CANONICALIZATIONS) { for (let current of replacement.splice(0)) { // A single variant can result in multiple variants, e.g.: // `[&>[data-selected]]:flex` → `*:data-selected:flex` - let result = fn(ds, cloneVariant(current)) + let result = fn(cloneVariant(current), options) if (Array.isArray(result)) { replacement.push(...result) continue @@ -122,7 +156,12 @@ const canonicalizeVariantCache = new DefaultMap((ds: DesignSystem) => { }) }) -const UTILITY_CANONICALIZATIONS = [ +type UtilityCanonicalizationFunction = ( + candidate: Candidate, + options: SignatureOptions, +) => Candidate + +const UTILITY_CANONICALIZATIONS: UtilityCanonicalizationFunction[] = [ bgGradientToLinear, themeToVarUtility, arbitraryUtilities, @@ -133,16 +172,17 @@ const UTILITY_CANONICALIZATIONS = [ optimizeModifier, ] -const canonicalizeUtilityCache = new DefaultMap((ds: DesignSystem) => { +const canonicalizeUtilityCache = new DefaultMap((options: SignatureOptions) => { + let designSystem = options.designSystem return new DefaultMap((rawCandidate: string): string => { - for (let readonlyCandidate of ds.parseCandidate(rawCandidate)) { + for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { let replacement = cloneCandidate(readonlyCandidate) as Writable for (let fn of UTILITY_CANONICALIZATIONS) { - replacement = fn(ds, replacement) + replacement = fn(replacement, options) } - let canonicalizedCandidate = ds.printCandidate(replacement) + let canonicalizedCandidate = designSystem.printCandidate(replacement) if (rawCandidate !== canonicalizedCandidate) { return canonicalizedCandidate } @@ -155,7 +195,7 @@ const canonicalizeUtilityCache = new DefaultMap((ds: DesignSystem) => { // ---- const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] -function bgGradientToLinear(_: DesignSystem, candidate: Candidate) { +function bgGradientToLinear(candidate: Candidate) { if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) { let direction = candidate.root.slice(15) @@ -178,8 +218,8 @@ const enum Convert { MigrateThemeOnly = 1 << 1, } -function themeToVarUtility(designSystem: DesignSystem, candidate: Candidate): Candidate { - let convert = converterCache.get(designSystem) +function themeToVarUtility(candidate: Candidate, options: SignatureOptions): Candidate { + let convert = converterCache.get(options.designSystem) if (candidate.kind === 'arbitrary') { let [newValue, modifier] = convert( @@ -210,8 +250,8 @@ function themeToVarUtility(designSystem: DesignSystem, candidate: Candidate): Ca return candidate } -function themeToVarVariant(designSystem: DesignSystem, variant: Variant): Variant | Variant[] { - let convert = converterCache.get(designSystem) +function themeToVarVariant(variant: Variant, options: SignatureOptions): Variant | Variant[] { + let convert = converterCache.get(options.designSystem) let iterator = walkVariants(variant) for (let [variant] of iterator) { @@ -545,7 +585,7 @@ const spacing = new DefaultMap | }) }) -function arbitraryUtilities(designSystem: DesignSystem, candidate: Candidate): Candidate { +function arbitraryUtilities(candidate: Candidate, options: SignatureOptions): Candidate { // We are only interested in arbitrary properties and arbitrary values if ( // Arbitrary property @@ -556,8 +596,9 @@ function arbitraryUtilities(designSystem: DesignSystem, candidate: Candidate): C return candidate } - let utilities = preComputedUtilities.get(designSystem) - let signatures = computeUtilitySignature.get(designSystem) + let designSystem = options.designSystem + let utilities = preComputedUtilities.get(options) + let signatures = computeUtilitySignature.get(options) let targetCandidateString = designSystem.printCandidate(candidate) @@ -761,14 +802,15 @@ function allVariablesAreUsed( // ---- -function bareValueUtilities(designSystem: DesignSystem, candidate: Candidate): Candidate { +function bareValueUtilities(candidate: Candidate, options: SignatureOptions): Candidate { // We are only interested in bare value utilities if (candidate.kind !== 'functional' || candidate.value?.kind !== 'named') { return candidate } - let utilities = preComputedUtilities.get(designSystem) - let signatures = computeUtilitySignature.get(designSystem) + let designSystem = options.designSystem + let utilities = preComputedUtilities.get(options) + let signatures = computeUtilitySignature.get(options) let targetCandidateString = designSystem.printCandidate(candidate) @@ -837,8 +879,9 @@ function bareValueUtilities(designSystem: DesignSystem, candidate: Candidate): C const DEPRECATION_MAP = new Map([['order-none', 'order-0']]) -function deprecatedUtilities(designSystem: DesignSystem, candidate: Candidate): Candidate { - let signatures = computeUtilitySignature.get(designSystem) +function deprecatedUtilities(candidate: Candidate, options: SignatureOptions): Candidate { + let designSystem = options.designSystem + let signatures = computeUtilitySignature.get(options) let targetCandidateString = printUnprefixedCandidate(designSystem, candidate) @@ -860,9 +903,10 @@ function deprecatedUtilities(designSystem: DesignSystem, candidate: Candidate): // ---- -function arbitraryVariants(designSystem: DesignSystem, variant: Variant): Variant | Variant[] { - let signatures = computeVariantSignature.get(designSystem) - let variants = preComputedVariants.get(designSystem) +function arbitraryVariants(variant: Variant, options: SignatureOptions): Variant | Variant[] { + let designSystem = options.designSystem + let signatures = computeVariantSignature.get(options) + let variants = preComputedVariants.get(options) let iterator = walkVariants(variant) for (let [variant] of iterator) { @@ -887,8 +931,9 @@ function arbitraryVariants(designSystem: DesignSystem, variant: Variant): Varian // ---- -function dropUnnecessaryDataTypes(designSystem: DesignSystem, candidate: Candidate): Candidate { - let signatures = computeUtilitySignature.get(designSystem) +function dropUnnecessaryDataTypes(candidate: Candidate, options: SignatureOptions): Candidate { + let designSystem = options.designSystem + let signatures = computeUtilitySignature.get(options) if ( candidate.kind === 'functional' && @@ -911,15 +956,16 @@ function dropUnnecessaryDataTypes(designSystem: DesignSystem, candidate: Candida // ---- function arbitraryValueToBareValueUtility( - designSystem: DesignSystem, candidate: Candidate, + options: SignatureOptions, ): Candidate { // We are only interested in functional utilities with arbitrary values if (candidate.kind !== 'functional' || candidate.value?.kind !== 'arbitrary') { return candidate } - let signatures = computeUtilitySignature.get(designSystem) + let designSystem = options.designSystem + let signatures = computeUtilitySignature.get(options) let expectedSignature = signatures.get(designSystem.printCandidate(candidate)) if (expectedSignature === null) return candidate @@ -935,7 +981,7 @@ function arbitraryValueToBareValueUtility( return candidate } -function arbitraryValueToBareValueVariant(_: DesignSystem, variant: Variant): Variant | Variant[] { +function arbitraryValueToBareValueVariant(variant: Variant): Variant | Variant[] { let iterator = walkVariants(variant) for (let [variant] of iterator) { // Convert `data-[selected]` to `data-selected` @@ -1083,11 +1129,12 @@ function isAttributeSelector(node: SelectorParser.SelectorAstNode): boolean { } function modernizeArbitraryValuesVariant( - designSystem: DesignSystem, variant: Variant, + options: SignatureOptions, ): Variant | Variant[] { let result = [variant] - let signatures = computeVariantSignature.get(designSystem) + let designSystem = options.designSystem + let signatures = computeVariantSignature.get(options) let iterator = walkVariants(variant) for (let [variant, parent] of iterator) { @@ -1442,7 +1489,7 @@ function modernizeArbitraryValuesVariant( // - `/[100%]` → `/100` → // - `/100` → // -function optimizeModifier(designSystem: DesignSystem, candidate: Candidate): Candidate { +function optimizeModifier(candidate: Candidate, options: SignatureOptions): Candidate { // We are only interested in functional or arbitrary utilities with a modifier if ( (candidate.kind !== 'functional' && candidate.kind !== 'arbitrary') || @@ -1451,7 +1498,8 @@ function optimizeModifier(designSystem: DesignSystem, candidate: Candidate): Can return candidate } - let signatures = computeUtilitySignature.get(designSystem) + let designSystem = options.designSystem + let signatures = computeUtilitySignature.get(options) let targetSignature = signatures.get(designSystem.printCandidate(candidate)) let modifier = candidate.modifier diff --git a/packages/tailwindcss/src/constant-fold-declaration.test.ts b/packages/tailwindcss/src/constant-fold-declaration.test.ts index 1fdbbde81402..b95ed74a340b 100644 --- a/packages/tailwindcss/src/constant-fold-declaration.test.ts +++ b/packages/tailwindcss/src/constant-fold-declaration.test.ts @@ -72,18 +72,18 @@ it.each([ it.each([ ['0deg', '0deg'], - ['0rad', '0rad'], + ['0rad', '0deg'], ['0%', '0%'], - ['0turn', '0turn'], + ['0turn', '0deg'], ['0fr', '0fr'], - ['0ms', '0ms'], + ['0ms', '0s'], ['0s', '0s'], ['-0.0deg', '0deg'], - ['-0.0rad', '0rad'], + ['-0.0rad', '0deg'], ['-0.0%', '0%'], - ['-0.0turn', '0turn'], + ['-0.0turn', '0deg'], ['-0.0fr', '0fr'], - ['-0.0ms', '0ms'], + ['-0.0ms', '0s'], ['-0.0s', '0s'], ])('should not fold non-foldable units to `0`. Constant fold `%s` into `%s`', (input, expected) => { expect(constantFoldDeclaration(input)).toBe(expected) diff --git a/packages/tailwindcss/src/constant-fold-declaration.ts b/packages/tailwindcss/src/constant-fold-declaration.ts index fc2424ef7eba..2ae6b2557c60 100644 --- a/packages/tailwindcss/src/constant-fold-declaration.ts +++ b/packages/tailwindcss/src/constant-fold-declaration.ts @@ -4,38 +4,26 @@ import * as ValueParser from './value-parser' // Assumption: We already assume that we receive somewhat valid `calc()` // expressions. So we will see `calc(1 + 1)` and not `calc(1+1)` -export function constantFoldDeclaration(input: string): string { +export function constantFoldDeclaration(input: string, rem: number | null): string { let folded = false let valueAst = ValueParser.parse(input) ValueParser.walkDepth(valueAst, (valueNode, { replaceWith }) => { - // Convert `-0`, `+0`, `0.0`, … to `0` - // Convert `-0px`, `+0em`, `0.0rem`, … to `0` + // Canonicalize dimensions to their simplest form. This includes: + // - Convert `-0`, `+0`, `0.0`, … to `0` + // - Convert `-0px`, `+0em`, `0.0rem`, … to `0` + // - Convert units to an equivalent unit if ( valueNode.kind === 'word' && - valueNode.value !== '0' && // Already `0`, nothing to do - ((valueNode.value[0] === '-' && valueNode.value[1] === '0') || // `-0…` - (valueNode.value[0] === '+' && valueNode.value[1] === '0') || // `+0…` - valueNode.value[0] === '0') // `0…` + valueNode.value !== '0' // Already `0`, nothing to do ) { - let dimension = dimensions.get(valueNode.value) - if (dimension === null) return // This shouldn't happen + let canonical = canonicalizeDimension(valueNode.value, rem) + if (canonical === null) return // Couldn't be canonicalized, nothing to do + if (canonical === valueNode.value) return // Already in canonical form, nothing to do - if (dimension[0] !== 0) return // Not a zero value, nothing to do - - // Replace length units with just `0` - if (dimension[1] === null || isLength(valueNode.value)) { - folded = true - replaceWith(ValueParser.word('0')) - return - } - - // Replace other units with `0`, e.g. `0%`, `0fr`, `0s`, … - else if (valueNode.value !== `0${dimension[1]}`) { - folded = true - replaceWith(ValueParser.word(`0${dimension[1]}`)) - return - } + folded = true + replaceWith(ValueParser.word(canonical)) + return } // Constant fold `calc()` expressions with two operands and one operator @@ -124,3 +112,39 @@ export function constantFoldDeclaration(input: string): string { return folded ? ValueParser.toCss(valueAst) : input } + +function canonicalizeDimension(input: string, rem: number | null = null): string | null { + let dimension = dimensions.get(input) + if (dimension === null) return null // This shouldn't happen + + let [value, unit] = dimension + if (unit === null) return `${value}` // Already unitless, nothing to do + + // Replace `0` units with just `0` + if (value === 0 && isLength(input)) return '0' + + // prettier-ignore + switch (unit.toLowerCase()) { + // to px, https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#lengths + case 'in': return `${value * 96}px` // 1in = 96.000px + case 'cm': return `${value * 96 / 2.54}px` // 1cm = 37.795px + case 'mm': return `${value * 96 / 2.54 / 10}px` // 1mm = 3.779px + case 'q': return `${value * 96 / 2.54 / 10 / 4}px` // 1q = 0.945px + case 'pc': return `${value * 96 / 6}px` // 1pc = 16.000px + case 'pt': return `${value * 96 / 72}px` // 1pt = 1.333px + case 'rem': return rem !== null ? `${value * rem}px` : null // 1rem = 16.000px (Assuming root font-size is 16px) + + // to deg, https://developer.mozilla.org/en-US/docs/Web/CSS/angle + case 'grad': return `${value * 0.9}deg` // 1grad = 0.900deg + case 'rad': return `${value * 180 / Math.PI}deg` // 1rad = 57.296deg + case 'turn': return `${value * 360}deg` // 1turn = 360.000deg + + //