diff --git a/CHANGELOG.md b/CHANGELOG.md index d253a01eddfe..a65b4c79c00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,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 diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index cfeda6ec4128..b8d729369d9b 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -210,27 +210,51 @@ function resolveFunctionKeys(object) { }, {}) } -function extractPluginConfigs(configs) { +function resolvePlugins(configs) { + let pluginGroups = [] let allConfigs = [] - configs.forEach((config) => { - allConfigs = [...allConfigs, config] - - const plugins = config?.plugins ?? [] - - if (plugins.length === 0) { - return - } + for (let config of configs) { + allConfigs.push(config) + + let plugins = [] + + 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 ) diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js index ac3547317133..0eeaabed9ab6 100644 --- a/tests/custom-plugins.test.js +++ b/tests/custom-plugins.test.js @@ -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; + } + `) +}) diff --git a/tests/fixtures/plugins/example.cjs b/tests/fixtures/plugins/example.cjs new file mode 100644 index 000000000000..c94746882b47 --- /dev/null +++ b/tests/fixtures/plugins/example.cjs @@ -0,0 +1,11 @@ +const plugin = require('../../../plugin.js') + +module.exports = plugin.withOptions(function ({ prefix = '' } = {}) { + return function ({ addUtilities }) { + addUtilities({ + [`.${prefix}example-1`]: { + color: 'red', + }, + }) + } +}) diff --git a/tests/fixtures/plugins/example.default.cjs b/tests/fixtures/plugins/example.default.cjs new file mode 100644 index 000000000000..499fb8c6d86c --- /dev/null +++ b/tests/fixtures/plugins/example.default.cjs @@ -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', + }, + }) + } +}) diff --git a/tests/resolveConfig.test.js b/tests/resolveConfig.test.js index e086986326e5..fd7c9bb73228 100644 --- a/tests/resolveConfig.test.js +++ b/tests/resolveConfig.test.js @@ -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], }) }) diff --git a/types/config.d.ts b/types/config.d.ts index 5cd04c5cc1b5..fb3652cf59a0 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -342,6 +342,8 @@ export type PluginsConfig = ( (options: any): { handler: PluginCreator; config?: Partial } __isOptionsFunction: true } + | string + | [string, Record] )[] // Top level config related