diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts new file mode 100644 index 000000000..f62d08359 --- /dev/null +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -0,0 +1,316 @@ +import { toCss, walk, type AstNode } from '../ast' +import type { DesignSystem } from '../design-system' +import type { Theme, ThemeKey } from '../theme' +import { withAlpha } from '../utilities' +import { segment } from '../utils/segment' +import { toKeyPath } from '../utils/to-key-path' +import { applyConfigToTheme } from './apply-config-to-theme' +import { createCompatConfig } from './config/create-compat-config' +import { resolveConfig } from './config/resolve-config' +import type { UserConfig } from './config/types' +import { darkModePlugin } from './dark-mode' +import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api' + +export async function applyCompatibilityHooks({ + designSystem, + ast, + loadPlugin, + loadConfig, + globs, +}: { + designSystem: DesignSystem + ast: AstNode[] + loadPlugin: (path: string) => Promise + loadConfig: (path: string) => Promise + globs: { origin?: string; pattern: string }[] +}) { + let pluginPaths: [string, CssPluginOptions | null][] = [] + let configPaths: string[] = [] + + walk(ast, (node, { parent, replaceWith }) => { + if (node.kind !== 'rule' || node.selector[0] !== '@') return + + // Collect paths from `@plugin` at-rules + if (node.selector === '@plugin' || node.selector.startsWith('@plugin ')) { + if (parent !== null) { + throw new Error('`@plugin` cannot be nested.') + } + + let pluginPath = node.selector.slice(9, -1) + if (pluginPath.length === 0) { + throw new Error('`@plugin` must have a path.') + } + + let options: CssPluginOptions = {} + + for (let decl of node.nodes ?? []) { + if (decl.kind !== 'declaration') { + throw new Error( + `Unexpected \`@plugin\` option:\n\n${toCss([decl])}\n\n\`@plugin\` options must be a flat list of declarations.`, + ) + } + + if (decl.value === undefined) continue + + // Parse the declaration value as a primitive type + // These are the same primitive values supported by JSON + let value: CssPluginOptions[keyof CssPluginOptions] = decl.value + + let parts = segment(value, ',').map((part) => { + part = part.trim() + + if (part === 'null') { + return null + } else if (part === 'true') { + return true + } else if (part === 'false') { + return false + } else if (!Number.isNaN(Number(part))) { + return Number(part) + } else if ( + (part[0] === '"' && part[part.length - 1] === '"') || + (part[0] === "'" && part[part.length - 1] === "'") + ) { + return part.slice(1, -1) + } else if (part[0] === '{' && part[part.length - 1] === '}') { + throw new Error( + `Unexpected \`@plugin\` option: Value of declaration \`${toCss([decl]).trim()}\` is not supported.\n\nUsing an object as a plugin option is currently only supported in JavaScript configuration files.`, + ) + } + + return part + }) + + options[decl.property] = parts.length === 1 ? parts[0] : parts + } + + pluginPaths.push([pluginPath, Object.keys(options).length > 0 ? options : null]) + + replaceWith([]) + return + } + + // Collect paths from `@config` at-rules + if (node.selector === '@config' || node.selector.startsWith('@config ')) { + if (node.nodes.length > 0) { + throw new Error('`@config` cannot have a body.') + } + + if (parent !== null) { + throw new Error('`@config` cannot be nested.') + } + + configPaths.push(node.selector.slice(9, -1)) + replaceWith([]) + return + } + }) + + // Override `resolveThemeValue` with a version that is backwards compatible + // with dot notation paths like `colors.red.500`. We could do this by default + // in `resolveThemeValue` but handling it here keeps all backwards + // compatibility concerns localized to our compatibility layer. + let resolveThemeVariableValue = designSystem.resolveThemeValue + + designSystem.resolveThemeValue = function resolveThemeValue(path: string) { + if (path.startsWith('--')) { + return resolveThemeVariableValue(path) + } + + // Extract an eventual modifier from the path. e.g.: + // - "colors.red.500 / 50%" -> "50%" + let lastSlash = path.lastIndexOf('/') + let modifier: string | null = null + if (lastSlash !== -1) { + modifier = path.slice(lastSlash + 1).trim() + path = path.slice(0, lastSlash).trim() as ThemeKey + } + + let themeValue = lookupThemeValue(designSystem.theme, path) + + // Apply the opacity modifier if present + if (modifier && themeValue) { + return withAlpha(themeValue, modifier) + } + + return themeValue + } + + // If there are no plugins or configs registered, we don't need to register + // any additional backwards compatibility hooks. + if (!pluginPaths.length && !configPaths.length) return + + let configs = await Promise.all( + configPaths.map(async (configPath) => ({ + path: configPath, + config: await loadConfig(configPath), + })), + ) + let pluginDetails = await Promise.all( + pluginPaths.map(async ([pluginPath, pluginOptions]) => ({ + path: pluginPath, + plugin: await loadPlugin(pluginPath), + options: pluginOptions, + })), + ) + + let plugins = pluginDetails.map((detail) => { + if (!detail.options) { + return detail.plugin + } + + if ('__isOptionsFunction' in detail.plugin) { + return detail.plugin(detail.options) + } + + throw new Error(`The plugin "${detail.path}" does not accept options`) + }) + + let userConfig = [{ config: { plugins } }, ...configs] + + let resolvedConfig = resolveConfig(designSystem, [ + { config: createCompatConfig(designSystem.theme) }, + ...userConfig, + { config: { plugins: [darkModePlugin] } }, + ]) + + let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig) + + for (let { handler } of resolvedConfig.plugins) { + handler(pluginApi) + } + + // Merge the user-configured theme keys into the design system. The compat + // config would otherwise expand into namespaces like `background-color` which + // core utilities already read from. + applyConfigToTheme(designSystem, userConfig) + + // Replace `resolveThemeValue` with a version that is backwards compatible + // with dot-notation but also aware of any JS theme configurations registered + // by plugins or JS config files. This is significantly slower than just + // upgrading dot-notation keys so we only use this version if plugins or + // config files are actually being used. In the future we may want to optimize + // this further by only doing this if plugins or config files _actually_ + // registered JS config objects. + designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) { + let resolvedValue = pluginApi.theme(path, defaultValue) + + if (Array.isArray(resolvedValue) && resolvedValue.length === 2) { + // When a tuple is returned, return the first element + return resolvedValue[0] + } else if (Array.isArray(resolvedValue)) { + // Arrays get serialized into a comma-separated lists + return resolvedValue.join(', ') + } else if (typeof resolvedValue === 'string') { + // Otherwise only allow string values here, objects (and namespace maps) + // are treated as non-resolved values for the CSS `theme()` function. + return resolvedValue + } + } + + for (let file of resolvedConfig.content.files) { + if ('raw' in file) { + throw new Error( + `Error in the config file/plugin/preset. The \`content\` key contains a \`raw\` entry:\n\n${JSON.stringify(file, null, 2)}\n\nThis feature is not currently supported.`, + ) + } + + globs.push({ origin: file.base, pattern: file.pattern }) + } +} + +function toThemeKey(keypath: string[]) { + return ( + keypath + // [1] should move into the nested object tuple. To create the CSS variable + // name for this, we replace it with an empty string that will result in two + // subsequent dashes when joined. + .map((path) => (path === '1' ? '' : path)) + + // Resolve the key path to a CSS variable segment + .map((part) => + part + .replaceAll('.', '_') + .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`), + ) + + // Remove the `DEFAULT` key at the end of a path + // We're reading from CSS anyway so it'll be a string + .filter((part, index) => part !== 'DEFAULT' || index !== keypath.length - 1) + .join('-') + ) +} + +function lookupThemeValue(theme: Theme, path: string) { + let baseThemeKey = '--' + toThemeKey(toKeyPath(path)) + + let resolvedValue = theme.get([baseThemeKey as ThemeKey]) + + if (resolvedValue !== null) { + return resolvedValue + } + + for (let [givenKey, upgradeKey] of Object.entries(themeUpgradeKeys)) { + if (!baseThemeKey.startsWith(givenKey)) continue + + let upgradedKey = upgradeKey + baseThemeKey.slice(givenKey.length) + let resolvedValue = theme.get([upgradedKey as ThemeKey]) + + if (resolvedValue !== null) { + return resolvedValue + } + } +} + +let themeUpgradeKeys = { + '--colors': '--color', + '--accent-color': '--color', + '--backdrop-blur': '--blur', + '--backdrop-brightness': '--brightness', + '--backdrop-contrast': '--contrast', + '--backdrop-grayscale': '--grayscale', + '--backdrop-hue-rotate': '--hueRotate', + '--backdrop-invert': '--invert', + '--backdrop-opacity': '--opacity', + '--backdrop-saturate': '--saturate', + '--backdrop-sepia': '--sepia', + '--background-color': '--color', + '--background-opacity': '--opacity', + '--border-color': '--color', + '--border-opacity': '--opacity', + '--border-spacing': '--spacing', + '--box-shadow-color': '--color', + '--caret-color': '--color', + '--divide-color': '--borderColor', + '--divide-opacity': '--borderOpacity', + '--divide-width': '--borderWidth', + '--fill': '--color', + '--flex-basis': '--spacing', + '--gap': '--spacing', + '--gradient-color-stops': '--color', + '--height': '--spacing', + '--inset': '--spacing', + '--margin': '--spacing', + '--max-height': '--spacing', + '--max-width': '--spacing', + '--min-height': '--spacing', + '--min-width': '--spacing', + '--outline-color': '--color', + '--padding': '--spacing', + '--placeholder-color': '--color', + '--placeholder-opacity': '--opacity', + '--ring-color': '--color', + '--ring-offset-color': '--color', + '--ring-opacity': '--opacity', + '--scroll-margin': '--spacing', + '--scroll-padding': '--spacing', + '--space': '--spacing', + '--stroke': '--color', + '--text-color': '--color', + '--text-decoration-color': '--color', + '--text-indent': '--spacing', + '--text-opacity': '--opacity', + '--translate': '--spacing', + '--size': '--spacing', + '--width': '--spacing', +} diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 0add9ccd7..4ea45b83f 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -1,5 +1,5 @@ import type { DesignSystem } from '../../design-system' -import type { PluginWithConfig } from '../../plugin-api' +import type { PluginWithConfig } from '../plugin-api' import { createThemeFn } from '../plugin-functions' import { deepMerge, isPlainObject } from './deep-merge' import { diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 593930240..5a194e96e 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -1,4 +1,4 @@ -import type { Plugin, PluginWithConfig } from '../../plugin-api' +import type { Plugin, PluginWithConfig } from '../plugin-api' import type { PluginUtils } from './resolve-config' export type ResolvableTo = T | ((utils: PluginUtils) => T) diff --git a/packages/tailwindcss/src/compat/dark-mode.ts b/packages/tailwindcss/src/compat/dark-mode.ts index 42061a0d6..0f9bc2cdf 100644 --- a/packages/tailwindcss/src/compat/dark-mode.ts +++ b/packages/tailwindcss/src/compat/dark-mode.ts @@ -1,5 +1,5 @@ -import type { PluginAPI } from '../plugin-api' import type { ResolvedConfig } from './config/types' +import type { PluginAPI } from './plugin-api' export function darkModePlugin({ addVariant, config }: PluginAPI) { let darkMode = config('darkMode', null) as ResolvedConfig['darkMode'] diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts similarity index 99% rename from packages/tailwindcss/src/plugin-api.test.ts rename to packages/tailwindcss/src/compat/plugin-api.test.ts index ca16ab977..1a11882f3 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1,9 +1,9 @@ import { describe, expect, test, vi } from 'vitest' -import { compile } from '.' -import defaultTheme from './compat/default-theme' -import plugin from './plugin' +import { compile } from '..' +import plugin from '../plugin' +import { optimizeCss } from '../test-utils/run' +import defaultTheme from './default-theme' import type { CssInJs, PluginAPI } from './plugin-api' -import { optimizeCss } from './test-utils/run' const css = String.raw diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts similarity index 62% rename from packages/tailwindcss/src/plugin-api.ts rename to packages/tailwindcss/src/compat/plugin-api.ts index 16f3a31c2..69fafbf1b 100644 --- a/packages/tailwindcss/src/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -1,21 +1,16 @@ -import { substituteAtApply } from './apply' -import { decl, rule, type AstNode } from './ast' -import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate' -import { applyConfigToTheme } from './compat/apply-config-to-theme' -import { createCompatConfig } from './compat/config/create-compat-config' -import { resolveConfig } from './compat/config/resolve-config' -import type { ResolvedConfig, UserConfig } from './compat/config/types' -import { darkModePlugin } from './compat/dark-mode' -import { createThemeFn } from './compat/plugin-functions' -import { substituteFunctions } from './css-functions' -import * as CSS from './css-parser' -import type { DesignSystem } from './design-system' -import type { Theme, ThemeKey } from './theme' -import { withAlpha, withNegative } from './utilities' -import { inferDataType } from './utils/infer-data-type' -import { segment } from './utils/segment' -import { toKeyPath } from './utils/to-key-path' -import { substituteAtSlot } from './variants' +import { substituteAtApply } from '../apply' +import { decl, rule, type AstNode } from '../ast' +import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate' +import { substituteFunctions } from '../css-functions' +import * as CSS from '../css-parser' +import type { DesignSystem } from '../design-system' +import { withAlpha, withNegative } from '../utilities' +import { inferDataType } from '../utils/infer-data-type' +import { segment } from '../utils/segment' +import { toKeyPath } from '../utils/to-key-path' +import { substituteAtSlot } from '../variants' +import type { ResolvedConfig, UserConfig } from './config/types' +import { createThemeFn } from './plugin-functions' export type Config = UserConfig export type PluginFn = (api: PluginAPI) => void @@ -82,7 +77,7 @@ export type PluginAPI = { const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ -function buildPluginApi( +export function buildPluginApi( designSystem: DesignSystem, ast: AstNode[], resolvedConfig: ResolvedConfig, @@ -438,229 +433,3 @@ function parseVariantValue(resolved: string | string[], nodes: AstNode[]): AstNo type Primitive = string | number | boolean | null export type CssPluginOptions = Record - -export async function applyCompatibilityHooks({ - designSystem, - ast, - pluginPaths, - loadPlugin, - configPaths, - loadConfig, - globs, -}: { - designSystem: DesignSystem - ast: AstNode[] - pluginPaths: [string, CssPluginOptions | null][] - loadPlugin: (path: string) => Promise - configPaths: string[] - loadConfig: (path: string) => Promise - globs: { origin?: string; pattern: string }[] -}) { - // Override `resolveThemeValue` with a version that is backwards compatible - // with dot notation paths like `colors.red.500`. We could do this by default - // in `resolveThemeValue` but handling it here keeps all backwards - // compatibility concerns localized to our compatibility layer. - let resolveThemeVariableValue = designSystem.resolveThemeValue - - designSystem.resolveThemeValue = function resolveThemeValue(path: string) { - if (path.startsWith('--')) { - return resolveThemeVariableValue(path) - } - - // Extract an eventual modifier from the path. e.g.: - // - "colors.red.500 / 50%" -> "50%" - let lastSlash = path.lastIndexOf('/') - let modifier: string | null = null - if (lastSlash !== -1) { - modifier = path.slice(lastSlash + 1).trim() - path = path.slice(0, lastSlash).trim() as ThemeKey - } - - let themeValue = lookupThemeValue(designSystem.theme, path) - - // Apply the opacity modifier if present - if (modifier && themeValue) { - return withAlpha(themeValue, modifier) - } - - return themeValue - } - - // If there are no plugins or configs registered, we don't need to register - // any additional backwards compatibility hooks. - if (!pluginPaths.length && !configPaths.length) return - - let configs = await Promise.all( - configPaths.map(async (configPath) => ({ - path: configPath, - config: await loadConfig(configPath), - })), - ) - let pluginDetails = await Promise.all( - pluginPaths.map(async ([pluginPath, pluginOptions]) => ({ - path: pluginPath, - plugin: await loadPlugin(pluginPath), - options: pluginOptions, - })), - ) - - let plugins = pluginDetails.map((detail) => { - if (!detail.options) { - return detail.plugin - } - - if ('__isOptionsFunction' in detail.plugin) { - return detail.plugin(detail.options) - } - - throw new Error(`The plugin "${detail.path}" does not accept options`) - }) - - let userConfig = [{ config: { plugins } }, ...configs] - - let resolvedConfig = resolveConfig(designSystem, [ - { config: createCompatConfig(designSystem.theme) }, - ...userConfig, - { config: { plugins: [darkModePlugin] } }, - ]) - - let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig) - - for (let { handler } of resolvedConfig.plugins) { - handler(pluginApi) - } - - // Merge the user-configured theme keys into the design system. The compat - // config would otherwise expand into namespaces like `background-color` which - // core utilities already read from. - applyConfigToTheme(designSystem, userConfig) - - // Replace `resolveThemeValue` with a version that is backwards compatible - // with dot-notation but also aware of any JS theme configurations registered - // by plugins or JS config files. This is significantly slower than just - // upgrading dot-notation keys so we only use this version if plugins or - // config files are actually being used. In the future we may want to optimize - // this further by only doing this if plugins or config files _actually_ - // registered JS config objects. - designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) { - let resolvedValue = pluginApi.theme(path, defaultValue) - - if (Array.isArray(resolvedValue) && resolvedValue.length === 2) { - // When a tuple is returned, return the first element - return resolvedValue[0] - } else if (Array.isArray(resolvedValue)) { - // Arrays get serialized into a comma-separated lists - return resolvedValue.join(', ') - } else if (typeof resolvedValue === 'string') { - // Otherwise only allow string values here, objects (and namespace maps) - // are treated as non-resolved values for the CSS `theme()` function. - return resolvedValue - } - } - - for (let file of resolvedConfig.content.files) { - if ('raw' in file) { - throw new Error( - `Error in the config file/plugin/preset. The \`content\` key contains a \`raw\` entry:\n\n${JSON.stringify(file, null, 2)}\n\nThis feature is not currently supported.`, - ) - } - - globs.push({ origin: file.base, pattern: file.pattern }) - } -} - -function toThemeKey(keypath: string[]) { - return ( - keypath - // [1] should move into the nested object tuple. To create the CSS variable - // name for this, we replace it with an empty string that will result in two - // subsequent dashes when joined. - .map((path) => (path === '1' ? '' : path)) - - // Resolve the key path to a CSS variable segment - .map((part) => - part - .replaceAll('.', '_') - .replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`), - ) - - // Remove the `DEFAULT` key at the end of a path - // We're reading from CSS anyway so it'll be a string - .filter((part, index) => part !== 'DEFAULT' || index !== keypath.length - 1) - .join('-') - ) -} - -function lookupThemeValue(theme: Theme, path: string) { - let baseThemeKey = '--' + toThemeKey(toKeyPath(path)) - - let resolvedValue = theme.get([baseThemeKey as ThemeKey]) - - if (resolvedValue !== null) { - return resolvedValue - } - - for (let [givenKey, upgradeKey] of Object.entries(themeUpgradeKeys)) { - if (!baseThemeKey.startsWith(givenKey)) continue - - let upgradedKey = upgradeKey + baseThemeKey.slice(givenKey.length) - let resolvedValue = theme.get([upgradedKey as ThemeKey]) - - if (resolvedValue !== null) { - return resolvedValue - } - } -} - -let themeUpgradeKeys = { - '--colors': '--color', - '--accent-color': '--color', - '--backdrop-blur': '--blur', - '--backdrop-brightness': '--brightness', - '--backdrop-contrast': '--contrast', - '--backdrop-grayscale': '--grayscale', - '--backdrop-hue-rotate': '--hueRotate', - '--backdrop-invert': '--invert', - '--backdrop-opacity': '--opacity', - '--backdrop-saturate': '--saturate', - '--backdrop-sepia': '--sepia', - '--background-color': '--color', - '--background-opacity': '--opacity', - '--border-color': '--color', - '--border-opacity': '--opacity', - '--border-spacing': '--spacing', - '--box-shadow-color': '--color', - '--caret-color': '--color', - '--divide-color': '--borderColor', - '--divide-opacity': '--borderOpacity', - '--divide-width': '--borderWidth', - '--fill': '--color', - '--flex-basis': '--spacing', - '--gap': '--spacing', - '--gradient-color-stops': '--color', - '--height': '--spacing', - '--inset': '--spacing', - '--margin': '--spacing', - '--max-height': '--spacing', - '--max-width': '--spacing', - '--min-height': '--spacing', - '--min-width': '--spacing', - '--outline-color': '--color', - '--padding': '--spacing', - '--placeholder-color': '--color', - '--placeholder-opacity': '--opacity', - '--ring-color': '--color', - '--ring-offset-color': '--color', - '--ring-opacity': '--opacity', - '--scroll-margin': '--spacing', - '--scroll-padding': '--spacing', - '--space': '--spacing', - '--stroke': '--color', - '--text-color': '--color', - '--text-decoration-color': '--color', - '--text-indent': '--spacing', - '--text-opacity': '--opacity', - '--translate': '--spacing', - '--size': '--spacing', - '--width': '--spacing', -} diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index d4fc57379..fb226708e 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -2,8 +2,8 @@ import fs from 'node:fs' import path from 'node:path' import { describe, expect, it, test } from 'vitest' import { compile } from '.' +import type { PluginAPI } from './compat/plugin-api' import plugin from './plugin' -import type { PluginAPI } from './plugin-api' import { compileCss, optimizeCss, run } from './test-utils/run' const css = String.raw diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 277ef12ff..8e6854ce0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,12 +1,13 @@ import { version } from '../package.json' import { substituteAtApply } from './apply' import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' +import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' +import { type Plugin } from './compat/plugin-api' import { compileCandidates } from './compile' import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions' import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' -import { applyCompatibilityHooks, type CssPluginOptions, type Plugin } from './plugin-api' import { Theme, ThemeOptions } from './theme' import { segment } from './utils/segment' export type Config = UserConfig @@ -50,8 +51,6 @@ async function parseCss( // Find all `@theme` declarations let theme = new Theme() - let pluginPaths: [string, CssPluginOptions | null][] = [] - let configPaths: string[] = [] let customVariants: ((designSystem: DesignSystem) => void)[] = [] let customUtilities: ((designSystem: DesignSystem) => void)[] = [] let firstThemeRule: Rule | null = null @@ -61,81 +60,6 @@ async function parseCss( walk(ast, (node, { parent, replaceWith }) => { if (node.kind !== 'rule') return - // Collect paths from `@plugin` at-rules - if (node.selector === '@plugin' || node.selector.startsWith('@plugin ')) { - if (parent !== null) { - throw new Error('`@plugin` cannot be nested.') - } - - let pluginPath = node.selector.slice(9, -1) - if (pluginPath.length === 0) { - throw new Error('`@plugin` must have a path.') - } - - let options: CssPluginOptions = {} - - for (let decl of node.nodes ?? []) { - if (decl.kind !== 'declaration') { - throw new Error( - `Unexpected \`@plugin\` option:\n\n${toCss([decl])}\n\n\`@plugin\` options must be a flat list of declarations.`, - ) - } - - if (decl.value === undefined) continue - - // Parse the declaration value as a primitive type - // These are the same primitive values supported by JSON - let value: CssPluginOptions[keyof CssPluginOptions] = decl.value - - let parts = segment(value, ',').map((part) => { - part = part.trim() - - if (part === 'null') { - return null - } else if (part === 'true') { - return true - } else if (part === 'false') { - return false - } else if (!Number.isNaN(Number(part))) { - return Number(part) - } else if ( - (part[0] === '"' && part[part.length - 1] === '"') || - (part[0] === "'" && part[part.length - 1] === "'") - ) { - return part.slice(1, -1) - } else if (part[0] === '{' && part[part.length - 1] === '}') { - throw new Error( - `Unexpected \`@plugin\` option: Value of declaration \`${toCss([decl]).trim()}\` is not supported.\n\nUsing an object as a plugin option is currently only supported in JavaScript configuration files.`, - ) - } - - return part - }) - - options[decl.property] = parts.length === 1 ? parts[0] : parts - } - - pluginPaths.push([pluginPath, Object.keys(options).length > 0 ? options : null]) - - replaceWith([]) - return - } - - // Collect paths from `@config` at-rules - if (node.selector === '@config' || node.selector.startsWith('@config ')) { - if (node.nodes.length > 0) { - throw new Error('`@config` cannot have a body.') - } - - if (parent !== null) { - throw new Error('`@config` cannot be nested.') - } - - configPaths.push(node.selector.slice(9, -1)) - replaceWith([]) - return - } - // Collect custom `@utility` at-rules if (node.selector.startsWith('@utility ')) { let name = node.selector.slice(9).trim() @@ -310,15 +234,7 @@ async function parseCss( // of random arguments because it really just needs access to "the world" to // do whatever ungodly things it needs to do to make things backwards // compatible without polluting core. - await applyCompatibilityHooks({ - designSystem, - ast, - pluginPaths, - loadPlugin, - configPaths, - loadConfig, - globs, - }) + await applyCompatibilityHooks({ designSystem, ast, loadPlugin, loadConfig, globs }) for (let customVariant of customVariants) { customVariant(designSystem) diff --git a/packages/tailwindcss/src/plugin.ts b/packages/tailwindcss/src/plugin.ts index 63d2ef8c6..c6731eb0f 100644 --- a/packages/tailwindcss/src/plugin.ts +++ b/packages/tailwindcss/src/plugin.ts @@ -1,4 +1,4 @@ -import type { Config, PluginFn, PluginWithConfig, PluginWithOptions } from './plugin-api' +import type { Config, PluginFn, PluginWithConfig, PluginWithOptions } from './compat/plugin-api' function createPlugin(handler: PluginFn, config?: Partial): PluginWithConfig { return {