Support loading plugins by package / file name (#12087)

* Cleanup test

Strings are going to actually do something now

* Refactor plugin loading in `resolveConfig`

This should keep the same instance of a plugin in use too. Maybe even reduce memory usage?! idk we’ll see

* Support simple plugin configs

* Update types

* Update changelog
This commit is contained in:
Jordan Pittman 2023-09-26 13:21:42 -04:00 • committed by GitHub
parent aaca7c4c4c
commit 05d0a6f8d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 26 deletions

View file

@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Explicitly configure Lightning CSS features, and prefer user browserslist over default browserslist ([#11402](https://github.com/tailwindlabs/tailwindcss/pull/11402), [#11412](https://github.com/tailwindlabs/tailwindcss/pull/11412))
- Extend default `opacity` scale to include all steps of 5 ([#11832](https://github.com/tailwindlabs/tailwindcss/pull/11832))
- Update Preflight `html` styles to include shadow DOM `:host` pseudo-class ([#11200](https://github.com/tailwindlabs/tailwindcss/pull/11200))
- Support loading plugins by package / file name ([#12087](https://github.com/tailwindlabs/tailwindcss/pull/12087))
### Changed

View file

@ -210,27 +210,51 @@ function resolveFunctionKeys(object) {
}, {})
}
function extractPluginConfigs(configs) {
function resolvePlugins(configs) {
let pluginGroups = []
let allConfigs = []
configs.forEach((config) => {
allConfigs = [...allConfigs, config]
for (let config of configs) {
allConfigs.push(config)
const plugins = config?.plugins ?? []
let plugins = []
if (plugins.length === 0) {
return
}
for (let plugin of config?.plugins ?? []) {
// TODO: If we want to support ESM plugins then a handful of things will have to become async
if (typeof plugin === 'string') {
// If the plugin is specified as a string then it's just the package name
plugin = require(plugin)
plugin = plugin.default ?? plugin
} else if (Array.isArray(plugin)) {
// If the plugin is specified as an array then it's a package name and optional options object
// [name] or [name, options]
let [pkg, options = undefined] = plugin
plugin = require(pkg)
plugin = plugin.default ?? plugin
plugin = plugin(options)
}
plugins.forEach((plugin) => {
if (plugin.__isOptionsFunction) {
plugin = plugin()
}
allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])]
})
})
return allConfigs
// We're explicitly skipping registering child plugins
// This will change in v4
let [, childConfigs] = resolvePlugins([plugin?.config ?? {}])
plugins.push(plugin)
allConfigs.push(...childConfigs)
}
pluginGroups.push(plugins)
}
// Reverse the order of the plugin groups
// This matches the old `reduceRight` behavior of the old `resolvePluginLists`
// Why? No idea.
let plugins = pluginGroups.reverse().flat()
return [plugins, allConfigs]
}
function resolveCorePlugins(corePluginConfigs) {
@ -244,17 +268,11 @@ function resolveCorePlugins(corePluginConfigs) {
return result
}
function resolvePluginLists(pluginLists) {
const result = [...pluginLists].reduceRight((resolved, pluginList) => {
return [...resolved, ...pluginList]
}, [])
return result
}
export default function resolveConfig(configs) {
let [plugins, pluginConfigs] = resolvePlugins(configs)
let allConfigs = [
...extractPluginConfigs(configs),
...pluginConfigs,
{
prefix: '',
important: false,
@ -269,7 +287,7 @@ export default function resolveConfig(configs) {
mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
),
corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
plugins,
},
...allConfigs
)

View file

@ -1914,3 +1914,63 @@ test('custom properties are not converted to kebab-case when added to base layer
expect(result.css).toContain(`--colors-primaryThing-500: 0, 0, 255;`)
})
})
test('plugins can loaded by package name / path', async () => {
let config = {
content: [{ raw: 'example-1' }],
plugins: [`${__dirname}/fixtures/plugins/example.cjs`],
}
let result = await run('@tailwind utilities', config)
expect(result.css).toMatchFormattedCss(css`
.example-1 {
color: red;
}
`)
})
test('named plugins can specify options', async () => {
let config = {
content: [{ raw: 'ex-example-1 example-1' }],
plugins: [[`${__dirname}/fixtures/plugins/example.cjs`, { prefix: 'ex-' }]],
}
let result = await run('@tailwind utilities', config)
expect(result.css).toMatchFormattedCss(css`
.ex-example-1 {
color: red;
}
`)
})
test('named plugins resolve default export', async () => {
let config = {
content: [{ raw: 'example-1' }],
plugins: [`${__dirname}/fixtures/plugins/example.default.cjs`],
}
let result = await run('@tailwind utilities', config)
expect(result.css).toMatchFormattedCss(css`
.example-1 {
color: red;
}
`)
})
test('named plugins resolve default export when using options', async () => {
let config = {
content: [{ raw: 'example-1 ex-example-1' }],
plugins: [[`${__dirname}/fixtures/plugins/example.default.cjs`, { prefix: 'ex-' }]],
}
let result = await run('@tailwind utilities', config)
expect(result.css).toMatchFormattedCss(css`
.ex-example-1 {
color: red;
}
`)
})

11
tests/fixtures/plugins/example.cjs vendored Normal file
View file

@ -0,0 +1,11 @@
const plugin = require('../../../plugin.js')
module.exports = plugin.withOptions(function ({ prefix = '' } = {}) {
return function ({ addUtilities }) {
addUtilities({
[`.${prefix}example-1`]: {
color: 'red',
},
})
}
})

View file

@ -0,0 +1,11 @@
const plugin = require('../../../plugin.js')
module.exports.default = plugin.withOptions(function ({ prefix = '' } = {}) {
return function ({ addUtilities }) {
addUtilities({
[`.${prefix}example-1`]: {
color: 'red',
},
})
}
})

View file

@ -1680,16 +1680,20 @@ test('core plugin configurations stack', () => {
})
test('plugins are merged', () => {
let p1 = { config: { order: '1' } }
let p2 = { config: { order: '2' } }
let p3 = { config: { order: '3' } }
const userConfig = {
plugins: ['3'],
plugins: [p3],
}
const otherConfig = {
plugins: ['2'],
plugins: [p2],
}
const defaultConfig = {
plugins: ['1'],
plugins: [p1],
prefix: '',
important: false,
separator: ':',
@ -1704,7 +1708,7 @@ test('plugins are merged', () => {
important: false,
separator: ':',
theme: {},
plugins: ['1', '2', '3'],
plugins: [p1, p2, p3],
})
})

2
types/config.d.ts vendored
View file

@ -342,6 +342,8 @@ export type PluginsConfig = (
(options: any): { handler: PluginCreator; config?: Partial<Config> }
__isOptionsFunction: true
}
| string
| [string, Record<string, any>]
)[]
// Top level config related