WIP
This commit is contained in:
parent
5035c106fb
commit
494eb1282d
2 changed files with 179 additions and 5 deletions
102
packages/tailwindcss/src/plugin-api.test.ts
Normal file
102
packages/tailwindcss/src/plugin-api.test.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue