From d8cef56dbd0fba37310aa63db8dc6a18381f6457 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 14 Aug 2024 12:45:10 -0400 Subject: [PATCH] wip --- packages/tailwindcss/src/ast.ts | 6 +-- .../tailwindcss/src/compat/config/types.ts | 8 ++-- packages/tailwindcss/src/plugin-api.test.ts | 40 ++++++++++++++++ packages/tailwindcss/src/plugin-api.ts | 48 ++++++++++++------- 4 files changed, 76 insertions(+), 26 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 6f4901c25..44567da39 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -48,7 +48,7 @@ export function objectToAst(obj: CssInJs): AstNode[] { let ast: AstNode[] = [] for (let [name, value] of Object.entries(obj)) { - if (typeof value === 'string') { + if (typeof value !== 'object') { if (!name.startsWith('--') && value === '@slot') { ast.push(rule(name, [rule('@slot', [])])) } else { @@ -56,9 +56,9 @@ export function objectToAst(obj: CssInJs): AstNode[] { // https://github.com/postcss/postcss-js/blob/b3db658b932b42f6ac14ca0b1d50f50c4569805b/parser.js#L30-L35 name = name.replace(/([A-Z])/g, '-$1').toLowerCase() - ast.push(decl(name, value)) + ast.push(decl(name, String(value))) } - } else { + } else if (value !== null) { ast.push(rule(name, objectToAst(value))) } } diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 4d7376632..54146309a 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -1,9 +1,9 @@ -import type { PluginAPI } from '../api' -import type { CorePluginName } from '../generated/core-utils' -import type { PluginUtils } from './utils' +import type { PluginUtils } from './resolve-config' export type ImportableFile = string | [path: string, options: Record] +type PluginAPI = {} + type FilePath = string type RawFile = { raw: string; extension?: string } export type RawPreset = Omit @@ -55,7 +55,6 @@ export interface UserConfig { presets?: Preset[] plugins?: Plugin[] theme?: ThemeConfig - corePlugins?: Partial> | CorePluginName[] | boolean } export type ThemeValue = ResolvableTo> | null | undefined @@ -74,5 +73,4 @@ export interface ResolvedConfig { darkMode: DarkModeConfig plugins: RawPlugin[] theme: Record> - corePlugins: Record } diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts index e9d4a0f38..fe4ea3b5f 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/plugin-api.test.ts @@ -255,4 +255,44 @@ describe('theme', async () => { " `) }) + + test('wip', async ({ expect }) => { + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let compiler = await compile(input, { + loadPlugin: async () => { + return plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + duration: (value) => ({ 'animation-duration': value }), + }, + { + values: theme('animationDuration'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, + }, + }, + ) + }, + }) + + expect(compiler.build(['duration-316'])).toMatchInlineSnapshot(` + ".duration-316 { + animation-duration: 316ms; + } + " + `) + }) }) diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts index 2021d3e6f..b77b875c0 100644 --- a/packages/tailwindcss/src/plugin-api.ts +++ b/packages/tailwindcss/src/plugin-api.ts @@ -3,10 +3,12 @@ import { objectToAst, rule, type AstNode, type CssInJs } from './ast' import { resolveConfig } from './compat/config/resolve-config' import type { UserConfig } from './compat/config/types' import type { DesignSystem } from './design-system' +import type { Theme } from './theme' import { withAlpha, withNegative } from './utilities' import { inferDataType } from './utils/infer-data-type' import { segment } from './utils/segment' +export type Config = UserConfig export type PluginFn = (api: PluginAPI) => void export type PluginWithConfig = { handler: PluginFn; config?: UserConfig } export type PluginWithOptions = { @@ -25,9 +27,7 @@ export type PluginAPI = { options?: Partial<{ type: string | string[] supportsNegativeValues: boolean - values: Record & { - [BARE_VALUE]?: (value: string) => string | null | undefined - } + values: Record modifiers: 'any' | Record }>, ): void @@ -136,7 +136,7 @@ export function buildPluginApi( let value: string | null = null { - let values = options?.values ?? {} + let values: Record = options?.values ?? {} if (isColor) { // Color utilities implicitly support `inherit`, `transparent`, and `currentColor` @@ -156,11 +156,17 @@ export function buildPluginApi( } else if (values[candidate.value.value]) { value = values[candidate.value.value] } else if (values[BARE_VALUE]) { - value = values[BARE_VALUE](candidate.value.value) ?? null + // We've snuk the bare value in here as a function even though values are + // typically only ever strings. This is a backwards compatibility hack. + let handleBareValue = values[BARE_VALUE] as unknown as ( + value: string, + ) => string | null + + value = handleBareValue(candidate.value.value) ?? null } } - if (!value) return + if (value === null) return // Resolve the modifier value let modifier: string | null @@ -182,12 +188,12 @@ export function buildPluginApi( } // A modifier was provided but is invalid - if (candidate.modifier && !modifier) { + if (candidate.modifier && modifier !== null) { // For arbitrary values, return `null` to avoid falling through to the next utility return candidate.value?.kind === 'arbitrary' ? null : undefined } - if (isColor && modifier) { + if (isColor && modifier !== null) { value = withAlpha(value, modifier) } @@ -224,7 +230,10 @@ export function buildPluginApi( } } -const BARE_VALUE = Symbol('BARE_VALUE') +// We can't use a symbol here because a lot of code in the ecosystem assumes an object with string / number keys +// for example using Object.entries() + Object.fromEntries() to modify an object which removes symbol keys +// We use `Math.random()` to ensure that users are unable to rely on the name of thiskey +const BARE_VALUE = `__BARE_VALUE__${Math.random().toString(36).slice(2)}` export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, ast: AstNode[]) { let pluginObjects = [] @@ -249,7 +258,7 @@ export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, a // Now merge all the configs and make all that crap work let resolvedConfig = resolveConfig([ - compatabilityConfig, + createCompatabilityConfig(designSystem.theme), ...pluginObjects.map(({ config }) => config ?? {}), ]) @@ -261,14 +270,17 @@ export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, a } } -export const compatabilityConfig = { - theme: { - transitionDuration: { - [BARE_VALUE]: (value: string) => { - if (!Number.isNaN(Number(value))) { - return `${value}ms` - } +export function createCompatabilityConfig(theme: Theme) { + return { + theme: { + transitionDuration: { + DEFAULT: theme.get(['--default-transition-duration']) ?? null, + [BARE_VALUE]: (value: string) => { + if (!Number.isNaN(Number(value))) { + return `${value}ms` + } + }, }, }, - }, + } }