Compare commits
5 commits
main
...
feat/v4-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
894d540deb | ||
|
|
fdd26011f1 | ||
|
|
103de984eb | ||
|
|
b6426b4f57 | ||
|
|
ec26eafbb4 |
6 changed files with 427 additions and 8 deletions
26
packages/tailwindcss/src/config.ts
Normal file
26
packages/tailwindcss/src/config.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { Plugin } from './plugins'
|
||||||
|
|
||||||
|
export interface UserConfig {
|
||||||
|
/**
|
||||||
|
* A list of registered plugins
|
||||||
|
*/
|
||||||
|
plugins?: Plugin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedConfig {
|
||||||
|
/**
|
||||||
|
* A list of registered plugins
|
||||||
|
*/
|
||||||
|
plugins: Plugin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolve(config: UserConfig) {
|
||||||
|
let defaults: ResolvedConfig = {
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { toCss } from './ast'
|
import { toCss } from './ast'
|
||||||
import { parseCandidate, parseVariant } from './candidate'
|
import { parseCandidate, parseVariant } from './candidate'
|
||||||
import { compileAstNodes, compileCandidates } from './compile'
|
import { compileAstNodes, compileCandidates } from './compile'
|
||||||
|
import type { ResolvedConfig } from './config'
|
||||||
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
|
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
|
||||||
|
import { registerPlugins, type Plugin } from './plugins'
|
||||||
import { getClassOrder } from './sort'
|
import { getClassOrder } from './sort'
|
||||||
import type { Theme } from './theme'
|
import type { Theme } from './theme'
|
||||||
import { Utilities, createUtilities } from './utilities'
|
import { Utilities, createUtilities } from './utilities'
|
||||||
|
|
@ -12,6 +14,7 @@ export type DesignSystem = {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
utilities: Utilities
|
utilities: Utilities
|
||||||
variants: Variants
|
variants: Variants
|
||||||
|
plugins: Plugin[]
|
||||||
|
|
||||||
candidatesToCss(classes: string[]): (string | null)[]
|
candidatesToCss(classes: string[]): (string | null)[]
|
||||||
getClassOrder(classes: string[]): [string, bigint | null][]
|
getClassOrder(classes: string[]): [string, bigint | null][]
|
||||||
|
|
@ -25,7 +28,7 @@ export type DesignSystem = {
|
||||||
getUsedVariants(): ReturnType<typeof parseVariant>[]
|
getUsedVariants(): ReturnType<typeof parseVariant>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDesignSystem(theme: Theme): DesignSystem {
|
export function buildDesignSystem(theme: Theme, config: ResolvedConfig): DesignSystem {
|
||||||
let utilities = createUtilities(theme)
|
let utilities = createUtilities(theme)
|
||||||
let variants = createVariants(theme)
|
let variants = createVariants(theme)
|
||||||
|
|
||||||
|
|
@ -37,6 +40,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||||
theme,
|
theme,
|
||||||
utilities,
|
utilities,
|
||||||
variants,
|
variants,
|
||||||
|
plugins: config.plugins,
|
||||||
|
|
||||||
candidatesToCss(classes: string[]) {
|
candidatesToCss(classes: string[]) {
|
||||||
let result: (string | null)[] = []
|
let result: (string | null)[] = []
|
||||||
|
|
@ -77,5 +81,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerPlugins(designSystem)
|
||||||
|
|
||||||
return designSystem
|
return designSystem
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
import { version } from '../package.json'
|
import { version } from '../package.json'
|
||||||
import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast'
|
import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast'
|
||||||
import { compileCandidates } from './compile'
|
import { compileCandidates } from './compile'
|
||||||
|
import { resolve, type UserConfig } from './config'
|
||||||
import * as CSS from './css-parser'
|
import * as CSS from './css-parser'
|
||||||
import { buildDesignSystem } from './design-system'
|
import { buildDesignSystem } from './design-system'
|
||||||
import { Theme } from './theme'
|
import { Theme } from './theme'
|
||||||
|
|
||||||
export function compile(css: string): {
|
export function compile(
|
||||||
|
css: string,
|
||||||
|
config?: UserConfig,
|
||||||
|
): {
|
||||||
build(candidates: string[]): string
|
build(candidates: string[]): string
|
||||||
|
readonly plugins: string[]
|
||||||
} {
|
} {
|
||||||
|
let resolved = resolve(config ?? {})
|
||||||
|
let plugins: string[] = []
|
||||||
|
|
||||||
let ast = CSS.parse(css)
|
let ast = CSS.parse(css)
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
|
@ -47,6 +55,20 @@ export function compile(css: string): {
|
||||||
replaceWith(node.nodes)
|
replaceWith(node.nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.selector.startsWith('@plugin')) {
|
||||||
|
let path = node.selector
|
||||||
|
// Ignore `@plugin` when parsing the selector
|
||||||
|
.slice(7)
|
||||||
|
// Remove any surrounding spaces
|
||||||
|
.trim()
|
||||||
|
// Remove the surrounding quotes
|
||||||
|
.slice(1, -1)
|
||||||
|
|
||||||
|
plugins.push(path)
|
||||||
|
replaceWith([])
|
||||||
|
return WalkAction.Skip
|
||||||
|
}
|
||||||
|
|
||||||
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
|
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
|
||||||
|
|
||||||
let isReference = node.selector === '@theme reference'
|
let isReference = node.selector === '@theme reference'
|
||||||
|
|
@ -125,7 +147,7 @@ export function compile(css: string): {
|
||||||
firstThemeRule.nodes = nodes
|
firstThemeRule.nodes = nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
let designSystem = buildDesignSystem(theme)
|
let designSystem = buildDesignSystem(theme, resolved)
|
||||||
|
|
||||||
let tailwindUtilitiesNode: Rule | null = null
|
let tailwindUtilitiesNode: Rule | null = null
|
||||||
|
|
||||||
|
|
@ -188,6 +210,7 @@ export function compile(css: string): {
|
||||||
let previousAstNodeCount = 0
|
let previousAstNodeCount = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
plugins,
|
||||||
build(newRawCandidates: string[]) {
|
build(newRawCandidates: string[]) {
|
||||||
let didChange = false
|
let didChange = false
|
||||||
|
|
||||||
|
|
@ -248,5 +271,5 @@ export function __unstable__loadDesignSystem(css: string) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return buildDesignSystem(theme)
|
return buildDesignSystem(theme, resolve({}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
packages/tailwindcss/src/plugins.test.ts
Normal file
177
packages/tailwindcss/src/plugins.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { test, vi } from 'vitest'
|
||||||
|
import { compile } from '.'
|
||||||
|
import type { UserConfig } from './config'
|
||||||
|
import type { Plugin } from './plugins'
|
||||||
|
import { compileWithPlugins, run } from './test-utils/run'
|
||||||
|
|
||||||
|
const css = String.raw
|
||||||
|
|
||||||
|
test('Plugins are called', ({ expect }) => {
|
||||||
|
let fn = vi.fn()
|
||||||
|
let handler = vi.fn()
|
||||||
|
let config: UserConfig = {
|
||||||
|
plugins: [fn, { handler }],
|
||||||
|
}
|
||||||
|
|
||||||
|
run([], config)
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
addUtility: expect.any(Function),
|
||||||
|
addUtilities: expect.any(Function),
|
||||||
|
addVariant: expect.any(Function),
|
||||||
|
addVariants: expect.any(Function),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
addUtility: expect.any(Function),
|
||||||
|
addUtilities: expect.any(Function),
|
||||||
|
addVariant: expect.any(Function),
|
||||||
|
addVariants: expect.any(Function),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Asynchronous plugins are waited on', async ({ expect }) => {
|
||||||
|
let before = vi.fn()
|
||||||
|
let after = vi.fn()
|
||||||
|
let config: UserConfig = {
|
||||||
|
plugins: [
|
||||||
|
async () => {
|
||||||
|
before()
|
||||||
|
await new Promise((resolve) => setTimeout(resolve))
|
||||||
|
after()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await run([], config)
|
||||||
|
|
||||||
|
expect(before).toHaveBeenCalled()
|
||||||
|
expect(after).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Custom static utilities', ({ expect }) => {
|
||||||
|
let config: UserConfig = {
|
||||||
|
plugins: [
|
||||||
|
({ addUtilities }) => {
|
||||||
|
addUtilities({
|
||||||
|
'.my-red': {
|
||||||
|
color: 'red',
|
||||||
|
'& > *': {
|
||||||
|
color: 'lightred',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.my-blue': {
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(run(['my-red'], config)).toMatchInlineSnapshot(`
|
||||||
|
".my-red {
|
||||||
|
color: red;
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
expect(run(['-my-red', 'my-red-[--value]'], config)).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Custom functional utilities', ({ expect }) => {
|
||||||
|
let config: UserConfig = {
|
||||||
|
plugins: [
|
||||||
|
({ addUtilities }) => {
|
||||||
|
addUtilities({
|
||||||
|
'.my-fn': (value, { modifier }) => ({
|
||||||
|
color: modifier ? `${value ?? ''} / ${modifier ?? ''}` : `${value ?? ''}`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(run(['my-fn', 'my-fn-[--value]', 'my-fn-[--value]/25'], config)).toMatchInlineSnapshot(`
|
||||||
|
".my-fn {
|
||||||
|
color: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-fn-\\[--value\\] {
|
||||||
|
color: var(--value);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-fn-\\[--value\\]\\/25 {
|
||||||
|
color: var(--value) / 25;
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Custom static variants', ({ expect }) => {
|
||||||
|
let config: UserConfig = {
|
||||||
|
plugins: [
|
||||||
|
({ addVariant }) => {
|
||||||
|
addVariant('hocus', ['&:hover', '&:focus'])
|
||||||
|
addVariant('hactive', ['&:hover', '&:active'])
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(run(['hocus:underline'], config)).toMatchInlineSnapshot(`
|
||||||
|
".hocus\\:underline:hover, .hocus\\:underline:focus {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
expect(run(['hover/foo:underline', 'hover-[123]:underline'], config)).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Custom functional variants', ({ expect }) => {
|
||||||
|
let config: UserConfig = {
|
||||||
|
plugins: [
|
||||||
|
({ addVariant }) => {
|
||||||
|
addVariant('in', ({ value, modifier }) => {
|
||||||
|
return modifier ? [] : value ? `&:is(${value} *)` : []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(run(['in-[:checked]:underline'], config)).toMatchInlineSnapshot(`
|
||||||
|
".in-\\[\\:checked\\]\\:underline:is(:checked *) {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
expect(run(['in-[:checked]/foo:underline'], config)).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Plugins registered in CSS can be returned', ({ expect }) => {
|
||||||
|
let builder = compile(css`
|
||||||
|
@plugin "my-plugin";
|
||||||
|
@plugin "./my-plugin";
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(builder.plugins).toEqual(['my-plugin', './my-plugin'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test.only('Plugins registered in CSS can be used', async ({ expect }) => {
|
||||||
|
vi.doMock('my-plugin', () => {
|
||||||
|
let plugin: Plugin = ({ addVariant }) => {
|
||||||
|
addVariant('hocus', ['&:hover', '&:focus'])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: plugin,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let result = await compileWithPlugins('@plugin "my-plugin"; @tailwind utilities;', [
|
||||||
|
'hocus:underline',
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
".hocus\\:underline:hover, .hocus\\:underline:focus {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}"
|
||||||
|
`)
|
||||||
|
})
|
||||||
169
packages/tailwindcss/src/plugins.ts
Normal file
169
packages/tailwindcss/src/plugins.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { decl, rule, type AstNode, type Rule } from './ast'
|
||||||
|
import type { Candidate } from './candidate'
|
||||||
|
import type { DesignSystem } from './design-system'
|
||||||
|
|
||||||
|
export type MatchData = {
|
||||||
|
value: string | null
|
||||||
|
modifier: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectorFormat = CssSelectors | ((candidate: MatchData) => CssSelectors)
|
||||||
|
export type UtilityFormat = CssTree | ((value: string | null, candidate: MatchData) => CssTree)
|
||||||
|
|
||||||
|
export type CssSelectors = string | string[]
|
||||||
|
export interface CssTree extends Record<string, string | CssTree> {}
|
||||||
|
|
||||||
|
export interface PluginAPI {
|
||||||
|
addVariant(name: string, format: SelectorFormat): void
|
||||||
|
addVariants(variants: Record<string, SelectorFormat>): void
|
||||||
|
|
||||||
|
addUtility(name: string, format: UtilityFormat): void
|
||||||
|
addUtilities(utilities: Record<string, UtilityFormat>): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Plugin = FunctionalPlugin | ConfigurablePlugin
|
||||||
|
|
||||||
|
export interface FunctionalPlugin {
|
||||||
|
(api: PluginAPI): void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigurablePlugin {
|
||||||
|
handler(api: PluginAPI): void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a recursive object structure to an AST
|
||||||
|
* Leaf nodes are declarations and branches are rules
|
||||||
|
**/
|
||||||
|
function treeToAst(props: CssTree): AstNode[] {
|
||||||
|
let ast: AstNode[] = []
|
||||||
|
|
||||||
|
for (let [name, value] of Object.entries(props)) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (value.startsWith('@')) {
|
||||||
|
ast.push(rule(name, []))
|
||||||
|
} else {
|
||||||
|
ast.push(decl(name, value))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ast.push(rule(name, treeToAst(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a recursive object structure to an AST
|
||||||
|
* Leaf nodes are declarations and branches are rules
|
||||||
|
**/
|
||||||
|
function selectorsToAst(r: Rule, selectors: CssSelectors): AstNode[] {
|
||||||
|
if (typeof selectors === 'string') {
|
||||||
|
return [rule(selectors, r.nodes)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectors.map((selector) => rule(selector, r.nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a recursive object structure to an AST
|
||||||
|
* Leaf nodes are declarations and branches are rules
|
||||||
|
**/
|
||||||
|
function utilityToAst(
|
||||||
|
candidate: Extract<Candidate, { kind: 'functional' | 'static' }>,
|
||||||
|
props: UtilityFormat,
|
||||||
|
): AstNode[] {
|
||||||
|
if (candidate.kind === 'static') {
|
||||||
|
if (candidate.negative) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props === 'object') {
|
||||||
|
return treeToAst(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props === 'object') {
|
||||||
|
return treeToAst(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
let match = {
|
||||||
|
value: candidate.value?.value ?? null,
|
||||||
|
modifier: candidate.modifier?.value ?? null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return treeToAst(props(match.value, match))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPlugins(design: DesignSystem) {
|
||||||
|
let { utilities, variants } = design
|
||||||
|
|
||||||
|
function addUtility(name: string, format: UtilityFormat) {
|
||||||
|
if (name.startsWith('.')) {
|
||||||
|
name = name.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof format === 'object') {
|
||||||
|
utilities.static(name, (candidate) => utilityToAst(candidate, format))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utilities.functional(name, (candidate) => utilityToAst(candidate, format))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUtilities(list: Record<string, UtilityFormat>) {
|
||||||
|
for (let [className, format] of Object.entries(list)) {
|
||||||
|
addUtility(className, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVariant(name: string, format: SelectorFormat) {
|
||||||
|
if (typeof format === 'string' || Array.isArray(format)) {
|
||||||
|
variants.static(
|
||||||
|
name,
|
||||||
|
(r) => {
|
||||||
|
r.nodes = selectorsToAst(r, format)
|
||||||
|
},
|
||||||
|
{ compounds: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
variants.functional(
|
||||||
|
name,
|
||||||
|
(r, variant) => {
|
||||||
|
let match = {
|
||||||
|
value: variant.value?.value ?? null,
|
||||||
|
modifier: variant.modifier?.value ?? null,
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolved = format(match)
|
||||||
|
|
||||||
|
r.nodes = selectorsToAst(r, resolved)
|
||||||
|
},
|
||||||
|
{ compounds: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVariants(list: Record<string, SelectorFormat>) {
|
||||||
|
for (let [name, format] of Object.entries(list)) {
|
||||||
|
addVariant(name, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let plugin of design.plugins) {
|
||||||
|
let handler = typeof plugin === 'function' ? plugin : plugin.handler
|
||||||
|
|
||||||
|
handler({
|
||||||
|
// v3 compatible legacy API
|
||||||
|
addUtility,
|
||||||
|
addUtilities,
|
||||||
|
addVariant,
|
||||||
|
addVariants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
import { Features, transform } from 'lightningcss'
|
import { Features, transform } from 'lightningcss'
|
||||||
import { compile } from '..'
|
import { compile } from '..'
|
||||||
|
import { type UserConfig } from '../config'
|
||||||
|
import type { Plugin } from '../plugins'
|
||||||
|
|
||||||
export function compileCss(css: string, candidates: string[] = []) {
|
export function compileCss(css: string, candidates: string[] = [], config: UserConfig = {}) {
|
||||||
return optimizeCss(compile(css).build(candidates)).trim()
|
return optimizeCss(compile(css, config).build(candidates)).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function run(candidates: string[]) {
|
export function run(candidates: string[], config: UserConfig = {}) {
|
||||||
return optimizeCss(compile('@tailwind utilities;').build(candidates)).trim()
|
return optimizeCss(compile('@tailwind utilities;', config).build(candidates)).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileWithPlugins(
|
||||||
|
css: string,
|
||||||
|
candidates: string[],
|
||||||
|
config: UserConfig = {},
|
||||||
|
) {
|
||||||
|
let builder = compile(css, config)
|
||||||
|
|
||||||
|
let plugins: Plugin[] = await Promise.all(
|
||||||
|
builder.plugins.map((pluginPath) => import(pluginPath).then((mod) => mod.default)),
|
||||||
|
)
|
||||||
|
|
||||||
|
config.plugins = [...(config.plugins ?? []), ...plugins]
|
||||||
|
|
||||||
|
return optimizeCss(builder.build(candidates)).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function optimizeCss(
|
export function optimizeCss(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue