This commit is contained in:
Adam Wathan 2024-08-13 11:39:08 -04:00
parent 5035c106fb
commit 494eb1282d
2 changed files with 179 additions and 5 deletions

View file

@ -0,0 +1,102 @@
import { test } from 'vitest'
import { compile } from '.'
import plugin from './plugin'
const css = String.raw
test('plugin', async ({ expect }) => {
let input = css`
@theme reference {
--color-red-100: #fee2e2;
--color-red-300: #fca5a5;
--color-red-500: #ef4444;
--color-red-700: #b91c1c;
--color-red-900: #7f1d1d;
--color-orange-100: #ffedd5;
--color-orange-300: #fdba74;
--color-orange-500: #f97316;
--color-orange-700: #c2410c;
--color-orange-900: #7c2d12;
}
@tailwind utilities;
@plugin "my-plugin";
`
let compiler = await compile(input, {
loadPlugin: async () => {
return plugin(
function ({ addUtilities, matchUtilities, theme }) {
addUtilities({
'@keyframes enter': theme('keyframes.enter'),
'@keyframes exit': theme('keyframes.exit'),
})
matchUtilities(
{
scrollbar: (value) => ({ 'scrollbar-color': value }),
},
{
values: theme('backgroundColor'),
},
)
},
{
theme: {
extend: {
colors: {
'russet-50': '#faf5ec',
'russet-100': '#f2e9cf',
'russet-200': '#e6d1a2',
'russet-300': '#d8b36c',
'russet-400': '#cb9744',
'russet-500': '#bc8236',
'russet-600': '#a2662c',
'russet-700': '#7a4724',
'russet-800': '#6d3f26',
'russet-900': '#5e3625',
'russet-950': '#361b12',
},
keyframes: {
enter: {
from: {
opacity: 'var(--tw-enter-opacity, 1)',
transform:
'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))',
},
},
exit: {
to: {
opacity: 'var(--tw-exit-opacity, 1)',
transform:
'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))',
},
},
},
},
},
},
)
},
})
expect(compiler.build(['scrollbar-red-500', 'scrollbar-russet-700'])).toMatchInlineSnapshot(`
".scrollbar-red-500 {
scrollbar-color: var(--color-red-500, #ef4444);
}
.scrollbar-russet-700 {
scrollbar-color: #7a4724;
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0));
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0));
}
}
"
`)
})

View file

@ -1,8 +1,10 @@
import { substituteAtApply } from './apply'
import { objectToAst, rule, type AstNode, type CssInJs } from './ast'
import type { DesignSystem } from './design-system'
import type { ThemeKey } from './theme'
import { withAlpha, withNegative } from './utilities'
import { inferDataType } from './utils/infer-data-type'
import { segment } from './utils/segment'
export type Config = Record<string, any>
@ -28,11 +30,16 @@ export type PluginAPI = {
modifiers: 'any' | Record<string, string>
}>,
): void
theme(path: string): any
}
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
export function buildPluginApi(designSystem: DesignSystem, ast: AstNode[]): PluginAPI {
export function buildPluginApi(
designSystem: DesignSystem,
ast: AstNode[],
resolvedConfig: { theme?: Record<string, any> },
): PluginAPI {
return {
addBase(css) {
ast.push(rule('@layer base', objectToAst(css)))
@ -61,6 +68,11 @@ export function buildPluginApi(designSystem: DesignSystem, ast: AstNode[]): Plug
addUtilities(utilities) {
for (let [name, css] of Object.entries(utilities)) {
if (name.startsWith('@keyframes ')) {
ast.push(rule(name, objectToAst(css)))
continue
}
if (name[0] !== '.' || !IS_VALID_UTILITY_NAME.test(name.slice(1))) {
throw new Error(
`\`addUtilities({ '${name}' : … })\` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`.`,
@ -186,27 +198,87 @@ export function buildPluginApi(designSystem: DesignSystem, ast: AstNode[]): Plug
})
}
},
theme(path) {
let parts = segment(path, '.')
let result = resolvedConfig.theme ?? {}
for (let part of parts) {
result = result?.[part]
}
if (
[
'accentColor',
'backgroundColor',
'borderColor',
'boxShadowColor',
'caretColor',
'divideColor',
'fill',
'gradientColorStops',
'outlineColor',
'placeholderColor',
'ringColor',
'ringOffsetColor',
'stroke',
'textColor',
'textDecorationColor',
].includes(path)
) {
// Convert camelCase to kebab-case:
// https://github.com/postcss/postcss-js/blob/b3db658b932b42f6ac14ca0b1d50f50c4569805b/parser.js#L30-L35
let newPath = `--${path.replace(/([A-Z])/g, '-$1').toLowerCase()}` as ThemeKey
for (let [key] of designSystem.theme.namespace('--color')) {
result[key ?? 'DEFAULT'] = designSystem.theme.resolve(key, ['--color'])
}
for (let [key] of designSystem.theme.namespace(newPath)) {
result[key ?? 'DEFAULT'] = designSystem.theme.resolve(key, [newPath])
}
}
return result
},
}
}
export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, ast: AstNode[]) {
let pluginApi = buildPluginApi(designSystem, ast)
let pluginObjects = []
for (let plugin of plugins) {
if ('__isOptionsFunction' in plugin) {
// Happens with `plugin.withOptions()` when no options were passed:
// e.g. `require("my-plugin")` instead of `require("my-plugin")(options)`
plugin().handler(pluginApi)
pluginObjects.push(plugin())
} else if ('handler' in plugin) {
// Happens with `plugin(…)`:
// e.g. `require("my-plugin")`
//
// or with `plugin.withOptions()` when the user passed options:
// e.g. `require("my-plugin")(options)`
plugin.handler(pluginApi)
pluginObjects.push(plugin)
} else {
// Just a plain function without using the plugin(…) API
plugin(pluginApi)
pluginObjects.push({ handler: plugin, config: {} as Config })
}
}
// Now merge all the configs and make all that crap work
let resolvedConfig: Config = { theme: {} }
for (let { config } of pluginObjects) {
for (let key in config?.theme?.extend) {
resolvedConfig.theme[key] ??= {}
Object.assign(resolvedConfig.theme[key], config?.theme.extend[key])
}
}
let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig)
// Loop over the handlers and run them all with the resolved config + CSS theme probably somehow
for (let { handler } of pluginObjects) {
handler(pluginApi)
}
}