Skip to content

Commit

Permalink
Add support for the tailwindcss/plugin export (#14173)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
thecrypticace and adamwathan authored Aug 13, 2024
1 parent 9ab4732 commit e299ea3
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

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

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

Expand Down
8 changes: 8 additions & 0 deletions packages/tailwindcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"require": "./dist/lib.js",
"import": "./src/index.ts"
},
"./plugin": {
"require": "./src/plugin.cts",
"import": "./src/plugin.ts"
},
"./package.json": "./package.json",
"./index.css": "./index.css",
"./index": "./index.css",
Expand All @@ -43,6 +47,10 @@
"require": "./dist/lib.js",
"import": "./dist/lib.mjs"
},
"./plugin": {
"require": "./dist/plugin.js",
"import": "./src/plugin.mjs"
},
"./package.json": "./package.json",
"./index.css": "./index.css",
"./index": "./index.css",
Expand Down
19 changes: 10 additions & 9 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, it, test } from 'vitest'
import { compile } from '.'
import type { PluginAPI } from './plugin-api'
import { compileCss, optimizeCss, run } from './test-utils/run'

const css = String.raw
Expand Down Expand Up @@ -1299,7 +1300,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
}
},
Expand All @@ -1317,7 +1318,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
}
},
Expand All @@ -1335,7 +1336,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
}
},
Expand Down Expand Up @@ -1366,7 +1367,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', ['&:hover', '&:focus'])
}
},
Expand Down Expand Up @@ -1398,7 +1399,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&:hover': '@slot',
'&:focus': '@slot',
Expand Down Expand Up @@ -1432,7 +1433,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'@media (hover: hover)': {
'&:hover': '@slot',
Expand Down Expand Up @@ -1480,7 +1481,7 @@ describe('plugins', () => {
`,
{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&': {
'--custom-property': '@slot',
Expand Down Expand Up @@ -1518,7 +1519,7 @@ describe('plugins', () => {

{
loadPlugin: async () => {
return ({ addVariant }) => {
return ({ addVariant }: PluginAPI) => {
addVariant('dark', '&:is([data-theme=dark] *)')
}
},
Expand Down Expand Up @@ -2087,7 +2088,7 @@ test('addBase', async () => {

{
loadPlugin: async () => {
return ({ addBase }) => {
return ({ addBase }: PluginAPI) => {
addBase({
body: {
'font-feature-settings': '"tnum"',
Expand Down
12 changes: 5 additions & 7 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import { WalkAction, comment, decl, rule, toCss, walk, type Rule } from './ast'
import { compileCandidates } from './compile'
import * as CSS from './css-parser'
import { buildDesignSystem, type DesignSystem } from './design-system'
import { buildPluginApi, type PluginAPI } from './plugin-api'
import { registerPlugins, type Plugin } from './plugin-api'
import { Theme } from './theme'
import { segment } from './utils/segment'

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

type Plugin = (api: PluginAPI) => void

type CompileOptions = {
loadPlugin?: (path: string) => Promise<Plugin>
}
Expand Down Expand Up @@ -40,7 +38,7 @@ async function parseCss(css: string, { loadPlugin = throwOnPlugin }: CompileOpti

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

pluginLoaders.push(loadPlugin(node.selector.slice(9, -1)))
pluginPaths.push(node.selector.slice(9, -1))
replaceWith([])
return
}
Expand Down Expand Up @@ -281,9 +279,9 @@ async function parseCss(css: string, { loadPlugin = throwOnPlugin }: CompileOpti
customUtility(designSystem)
}

let pluginApi = buildPluginApi(designSystem, ast)
let plugins = await Promise.all(pluginPaths.map(loadPlugin))

await Promise.all(pluginLoaders.map((loader) => loader.then((plugin) => plugin(pluginApi))))
registerPlugins(plugins, designSystem, ast)

// Replace `@apply` rules with the actual utility classes.
if (css.includes('@apply')) {
Expand Down
33 changes: 33 additions & 0 deletions packages/tailwindcss/src/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import type { DesignSystem } from './design-system'
import { withAlpha, withNegative } from './utilities'
import { inferDataType } from './utils/infer-data-type'

export type Config = Record<string, any>

export type PluginFn = (api: PluginAPI) => void
export type PluginWithConfig = { handler: PluginFn; config?: Partial<Config> }
export type PluginWithOptions<T> = {
(options?: T): PluginWithConfig
__isOptionsFunction: true
}

export type Plugin = PluginFn | PluginWithConfig | PluginWithOptions<any>

export type PluginAPI = {
addBase(base: CssInJs): void
addVariant(name: string, variant: string | string[] | CssInJs): void
Expand Down Expand Up @@ -177,3 +188,25 @@ export function buildPluginApi(designSystem: DesignSystem, ast: AstNode[]): Plug
},
}
}

export function registerPlugins(plugins: Plugin[], designSystem: DesignSystem, ast: AstNode[]) {
let pluginApi = buildPluginApi(designSystem, ast)

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)
} 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)
} else {
// Just a plain function without using the plugin(…) API
plugin(pluginApi)
}
}
}
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/plugin.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This file exists so that `plugin.ts` can be written one time but be compatible with both CJS and
// ESM. Without it we get a `.default` export when using `require` in CJS.

// @ts-ignore
module.exports = require('./plugin.ts').default
61 changes: 61 additions & 0 deletions packages/tailwindcss/src/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { test } from 'vitest'
import { compile } from '.'
import plugin from './plugin'

const css = String.raw

test('plugin', async ({ expect }) => {
let input = css`
@plugin "my-plugin";
`

let compiler = await compile(input, {
loadPlugin: async () => {
return plugin(function ({ addBase }) {
addBase({
body: {
margin: '0',
},
})
})
},
})

expect(compiler.build([])).toMatchInlineSnapshot(`
"@layer base {
body {
margin: 0;
}
}
"
`)
})

test('plugin.withOptions', async ({ expect }) => {
let input = css`
@plugin "my-plugin";
`

let compiler = await compile(input, {
loadPlugin: async () => {
return plugin.withOptions(function (opts = { foo: '1px' }) {
return function ({ addBase }) {
addBase({
body: {
margin: opts.foo,
},
})
}
})
},
})

expect(compiler.build([])).toMatchInlineSnapshot(`
"@layer base {
body {
margin: 1px;
}
}
"
`)
})
26 changes: 26 additions & 0 deletions packages/tailwindcss/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Config, PluginFn, PluginWithConfig, PluginWithOptions } from './plugin-api'

function createPlugin(handler: PluginFn, config?: Partial<Config>): PluginWithConfig {
return {
handler,
config,
}
}

createPlugin.withOptions = function <T>(
pluginFunction: (options?: T) => PluginFn,
configFunction: (options?: T) => Partial<Config> = () => ({}),
): PluginWithOptions<T> {
function optionsFunction(options: T): PluginWithConfig {
return {
handler: pluginFunction(options),
config: configFunction(options),
}
}

optionsFunction.__isOptionsFunction = true as const

return optionsFunction as PluginWithOptions<T>
}

export default createPlugin
Loading

0 comments on commit e299ea3

Please sign in to comment.