Skip to content

Commit e299ea3

Browse files
Add support for the tailwindcss/plugin export (#14173)
This PR adds support for the `tailwindcss/plugin` import which has historically been used to define custom plugins: ```js import plugin from "tailwindcss/plugin"; export default plugin(function ({ addBase }) { addBase({ // ... }); }); ``` This also adds support for `plugin.withOptions` which was used to define plugins that took optional initilization options when they were registered in your `tailwind.config.js` file: ```js import plugin from "tailwindcss/plugin"; export default plugin.withOptions((options = {}) => { return function ({ addBase }) { addBase({ // ... }); }; }); ``` We've stubbed out support for the `config` argument but we're not actually doing anything with it at the time of this PR. The scope of this PR is just to allow people to create plugins that currently work using the raw function syntax but using the `plugin` and `plugin.withOptions` APIs. Support for `config` will land separately. --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 9ab4732 commit e299ea3

File tree

10 files changed

+191
-39
lines changed

10 files changed

+191
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Add support for `addBase` plugins using the `@plugin` directive ([#14172](https://github.com/tailwindlabs/tailwindcss/pull/14172))
13+
- Add support for the `tailwindcss/plugin` export ([#14173](https://github.com/tailwindlabs/tailwindcss/pull/14173))
1314

1415
## [4.0.0-alpha.19] - 2024-08-09
1516

packages/tailwindcss/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"require": "./dist/lib.js",
2424
"import": "./src/index.ts"
2525
},
26+
"./plugin": {
27+
"require": "./src/plugin.cts",
28+
"import": "./src/plugin.ts"
29+
},
2630
"./package.json": "./package.json",
2731
"./index.css": "./index.css",
2832
"./index": "./index.css",
@@ -43,6 +47,10 @@
4347
"require": "./dist/lib.js",
4448
"import": "./dist/lib.mjs"
4549
},
50+
"./plugin": {
51+
"require": "./dist/plugin.js",
52+
"import": "./src/plugin.mjs"
53+
},
4654
"./package.json": "./package.json",
4755
"./index.css": "./index.css",
4856
"./index": "./index.css",

packages/tailwindcss/src/index.test.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33
import { describe, expect, it, test } from 'vitest'
44
import { compile } from '.'
5+
import type { PluginAPI } from './plugin-api'
56
import { compileCss, optimizeCss, run } from './test-utils/run'
67

78
const css = String.raw
@@ -1299,7 +1300,7 @@ describe('plugins', () => {
12991300
`,
13001301
{
13011302
loadPlugin: async () => {
1302-
return ({ addVariant }) => {
1303+
return ({ addVariant }: PluginAPI) => {
13031304
addVariant('hocus', '&:hover, &:focus')
13041305
}
13051306
},
@@ -1317,7 +1318,7 @@ describe('plugins', () => {
13171318
`,
13181319
{
13191320
loadPlugin: async () => {
1320-
return ({ addVariant }) => {
1321+
return ({ addVariant }: PluginAPI) => {
13211322
addVariant('hocus', '&:hover, &:focus')
13221323
}
13231324
},
@@ -1335,7 +1336,7 @@ describe('plugins', () => {
13351336
`,
13361337
{
13371338
loadPlugin: async () => {
1338-
return ({ addVariant }) => {
1339+
return ({ addVariant }: PluginAPI) => {
13391340
addVariant('hocus', '&:hover, &:focus')
13401341
}
13411342
},
@@ -1366,7 +1367,7 @@ describe('plugins', () => {
13661367
`,
13671368
{
13681369
loadPlugin: async () => {
1369-
return ({ addVariant }) => {
1370+
return ({ addVariant }: PluginAPI) => {
13701371
addVariant('hocus', ['&:hover', '&:focus'])
13711372
}
13721373
},
@@ -1398,7 +1399,7 @@ describe('plugins', () => {
13981399
`,
13991400
{
14001401
loadPlugin: async () => {
1401-
return ({ addVariant }) => {
1402+
return ({ addVariant }: PluginAPI) => {
14021403
addVariant('hocus', {
14031404
'&:hover': '@slot',
14041405
'&:focus': '@slot',
@@ -1432,7 +1433,7 @@ describe('plugins', () => {
14321433
`,
14331434
{
14341435
loadPlugin: async () => {
1435-
return ({ addVariant }) => {
1436+
return ({ addVariant }: PluginAPI) => {
14361437
addVariant('hocus', {
14371438
'@media (hover: hover)': {
14381439
'&:hover': '@slot',
@@ -1480,7 +1481,7 @@ describe('plugins', () => {
14801481
`,
14811482
{
14821483
loadPlugin: async () => {
1483-
return ({ addVariant }) => {
1484+
return ({ addVariant }: PluginAPI) => {
14841485
addVariant('hocus', {
14851486
'&': {
14861487
'--custom-property': '@slot',
@@ -1518,7 +1519,7 @@ describe('plugins', () => {
15181519

15191520
{
15201521
loadPlugin: async () => {
1521-
return ({ addVariant }) => {
1522+
return ({ addVariant }: PluginAPI) => {
15221523
addVariant('dark', '&:is([data-theme=dark] *)')
15231524
}
15241525
},
@@ -2087,7 +2088,7 @@ test('addBase', async () => {
20872088

20882089
{
20892090
loadPlugin: async () => {
2090-
return ({ addBase }) => {
2091+
return ({ addBase }: PluginAPI) => {
20912092
addBase({
20922093
body: {
20932094
'font-feature-settings': '"tnum"',

packages/tailwindcss/src/index.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ import { WalkAction, comment, decl, rule, toCss, walk, type Rule } from './ast'
44
import { compileCandidates } from './compile'
55
import * as CSS from './css-parser'
66
import { buildDesignSystem, type DesignSystem } from './design-system'
7-
import { buildPluginApi, type PluginAPI } from './plugin-api'
7+
import { registerPlugins, type Plugin } from './plugin-api'
88
import { Theme } from './theme'
99
import { segment } from './utils/segment'
1010

1111
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
1212

13-
type Plugin = (api: PluginAPI) => void
14-
1513
type CompileOptions = {
1614
loadPlugin?: (path: string) => Promise<Plugin>
1715
}
@@ -40,7 +38,7 @@ async function parseCss(css: string, { loadPlugin = throwOnPlugin }: CompileOpti
4038

4139
// Find all `@theme` declarations
4240
let theme = new Theme()
43-
let pluginLoaders: Promise<Plugin>[] = []
41+
let pluginPaths: string[] = []
4442
let customVariants: ((designSystem: DesignSystem) => void)[] = []
4543
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
4644
let firstThemeRule: Rule | null = null
@@ -60,7 +58,7 @@ async function parseCss(css: string, { loadPlugin = throwOnPlugin }: CompileOpti
6058
throw new Error('`@plugin` cannot be nested.')
6159
}
6260

63-
pluginLoaders.push(loadPlugin(node.selector.slice(9, -1)))
61+
pluginPaths.push(node.selector.slice(9, -1))
6462
replaceWith([])
6563
return
6664
}
@@ -281,9 +279,9 @@ async function parseCss(css: string, { loadPlugin = throwOnPlugin }: CompileOpti
281279
customUtility(designSystem)
282280
}
283281

284-
let pluginApi = buildPluginApi(designSystem, ast)
282+
let plugins = await Promise.all(pluginPaths.map(loadPlugin))
285283

286-
await Promise.all(pluginLoaders.map((loader) => loader.then((plugin) => plugin(pluginApi))))
284+
registerPlugins(plugins, designSystem, ast)
287285

288286
// Replace `@apply` rules with the actual utility classes.
289287
if (css.includes('@apply')) {

packages/tailwindcss/src/plugin-api.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import type { DesignSystem } from './design-system'
44
import { withAlpha, withNegative } from './utilities'
55
import { inferDataType } from './utils/infer-data-type'
66

7+
export type Config = Record<string, any>
8+
9+
export type PluginFn = (api: PluginAPI) => void
10+
export type PluginWithConfig = { handler: PluginFn; config?: Partial<Config> }
11+
export type PluginWithOptions<T> = {
12+
(options?: T): PluginWithConfig
13+
__isOptionsFunction: true
14+
}
15+
16+
export type Plugin = PluginFn | PluginWithConfig | PluginWithOptions<any>
17+
718
export type PluginAPI = {
819
addBase(base: CssInJs): void
920
addVariant(name: string, variant: string | string[] | CssInJs): void
@@ -177,3 +188,25 @@ export function buildPluginApi(designSystem: DesignSystem, ast: AstNode[]): Plug
177188
},
178189
}
179190
}
191+
192+
export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, ast: AstNode[]) {
193+
let pluginApi = buildPluginApi(designSystem, ast)
194+
195+
for (let plugin of plugins) {
196+
if ('__isOptionsFunction' in plugin) {
197+
// Happens with `plugin.withOptions()` when no options were passed:
198+
// e.g. `require("my-plugin")` instead of `require("my-plugin")(options)`
199+
plugin().handler(pluginApi)
200+
} else if ('handler' in plugin) {
201+
// Happens with `plugin(…)`:
202+
// e.g. `require("my-plugin")`
203+
//
204+
// or with `plugin.withOptions()` when the user passed options:
205+
// e.g. `require("my-plugin")(options)`
206+
plugin.handler(pluginApi)
207+
} else {
208+
// Just a plain function without using the plugin(…) API
209+
plugin(pluginApi)
210+
}
211+
}
212+
}

packages/tailwindcss/src/plugin.cts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// This file exists so that `plugin.ts` can be written one time but be compatible with both CJS and
2+
// ESM. Without it we get a `.default` export when using `require` in CJS.
3+
4+
// @ts-ignore
5+
module.exports = require('./plugin.ts').default
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { test } from 'vitest'
2+
import { compile } from '.'
3+
import plugin from './plugin'
4+
5+
const css = String.raw
6+
7+
test('plugin', async ({ expect }) => {
8+
let input = css`
9+
@plugin "my-plugin";
10+
`
11+
12+
let compiler = await compile(input, {
13+
loadPlugin: async () => {
14+
return plugin(function ({ addBase }) {
15+
addBase({
16+
body: {
17+
margin: '0',
18+
},
19+
})
20+
})
21+
},
22+
})
23+
24+
expect(compiler.build([])).toMatchInlineSnapshot(`
25+
"@layer base {
26+
body {
27+
margin: 0;
28+
}
29+
}
30+
"
31+
`)
32+
})
33+
34+
test('plugin.withOptions', async ({ expect }) => {
35+
let input = css`
36+
@plugin "my-plugin";
37+
`
38+
39+
let compiler = await compile(input, {
40+
loadPlugin: async () => {
41+
return plugin.withOptions(function (opts = { foo: '1px' }) {
42+
return function ({ addBase }) {
43+
addBase({
44+
body: {
45+
margin: opts.foo,
46+
},
47+
})
48+
}
49+
})
50+
},
51+
})
52+
53+
expect(compiler.build([])).toMatchInlineSnapshot(`
54+
"@layer base {
55+
body {
56+
margin: 1px;
57+
}
58+
}
59+
"
60+
`)
61+
})

packages/tailwindcss/src/plugin.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Config, PluginFn, PluginWithConfig, PluginWithOptions } from './plugin-api'
2+
3+
function createPlugin(handler: PluginFn, config?: Partial<Config>): PluginWithConfig {
4+
return {
5+
handler,
6+
config,
7+
}
8+
}
9+
10+
createPlugin.withOptions = function <T>(
11+
pluginFunction: (options?: T) => PluginFn,
12+
configFunction: (options?: T) => Partial<Config> = () => ({}),
13+
): PluginWithOptions<T> {
14+
function optionsFunction(options: T): PluginWithConfig {
15+
return {
16+
handler: pluginFunction(options),
17+
config: configFunction(options),
18+
}
19+
}
20+
21+
optionsFunction.__isOptionsFunction = true as const
22+
23+
return optionsFunction as PluginWithOptions<T>
24+
}
25+
26+
export default createPlugin

0 commit comments

Comments
 (0)