Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow plugins to add their own config file to be resolved with the user's custom config #1162

Merged
merged 15 commits into from
Oct 14, 2019

Conversation

adamwathan
Copy link
Member

@adamwathan adamwathan commented Oct 11, 2019

This PR makes it possible for third-party plugins to provide a config object in the same format as Tailwind's regular config object that will be intelligently merged with the user's config as part of the config resolution process.

This is useful when a plugin is adding new utilities and wants to provide default theme values for those utilities that the user can still override or extend the same way they would built-in utilities in Tailwind.

To understand why this is helpful, consider this hypothetical @tailwindcss/rotate plugin:

// plugin.js
module.exports = function ({ addUtilities, theme, variants }) {
  const rotateConfig = theme('rotate', {
    '0': '0deg',
    '90': '90deg',
    '180': '180deg',
    '270': '270deg',
  })

  const rotateUtilities = /* ... */

  addUtilities(rotateUtilities, variants('rotate'))
}

It looks in the user's config for a rotate key, and if not present, provides four default rotate values.

Now consider this Tailwind config file that uses this plugin:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      rotate: {
        '45': '45deg'
      }
    }
  },
  plugins: [
    require('@tailwindcss/rotate')
  ]
}

You might think that because the user has specified the new 45 value in the extend section, this would add the rotate-45 utility in addition to the default utilities provided by the plugin, but it doesn't. Instead, the rotate-45 utility is the only one generated, because the theme() function exposed to plugins only allows a plugin author to check for the presence of a key and provide a default if the key isn't there.

This PR fixes this problem by allowing plugins to provide a configuration object that Tailwind should intelligently merge into the final config representation, before plugins are properly processed so that features like extend work as you might expect even with custom theme keys depended on by plugins.

This is made possible by allowing plugins to now be represented as objects in addition to functions, where the object can have a config key, and a handler key, which is where you'd put the code you'd normally put directly in the plugin function

For example, here's a plugin that adds rotate utilities:

// rotate-plugin.js
module.exports = {
  config: {
    theme: {
      rotate: {
        '0': '0deg',
        '90': '90deg',
        '180': '180deg',
        '270': '270deg',
      },
    },
    variants: {
      rotate: ['responsive']
    }
  },
  handler({ addUtilities, theme, variants, e }) {
    const rotateUtilities = Object.entries(theme('rotate')).map(([key, value]) => {
      return {
        [`.${e(`rotate-${key}`)}`]: {
          transform: `rotate(${value})`,
        },
      }
    })
    addUtilities(rotateUtilities, variants('rotate'))
  }
}

By using this new feature and writing this plugin this way, the user can extend or override the default rotate values just like they can with Tailwind's default config:

Override the defaults

// tailwind.config.js
module.exports = {
  theme: {
    rotate: {
      quarter: '90deg'
      half: '180deg'
    }
  },
  plugins: [
    require('@tailwindcss/rotate')
  ]
}

Extend the defaults

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      rotate: {
        '45': '45deg'
      }
    }
  },
  plugins: [
    require('@tailwindcss/rotate')
  ]
}

This was implemented by making it possible for Tailwind to resolve multiple config objects under the hood, which is now also exposed to userland in the resolveConfig function.

Config objects need to passed in to the resolveConfig function starting with the highest priority config, and ending with the lowest priority:

const finalConfig = resolveConfig(mostImportantConfig, nextImportantConfig, lastImportandConfig)

In the resolveConfig function available by importing tailwindcss/resolveConfig, the default config object is always added to the end of the list. Eventually you will be able to override this, likely using a new extends property or similar directly in your config where you can specify which configs your config should extend.

@adamwathan adamwathan force-pushed the allow-plugins-to-modify-config branch from 71d59b9 to 53dff62 Compare October 12, 2019 17:41
@adamwathan adamwathan changed the title Allow plugins to modify the config Allow plugins to add their own config file to be resolved with the user's custom config Oct 12, 2019
@adamwathan adamwathan merged commit 7a7303b into master Oct 14, 2019
@adamwathan
Copy link
Member Author

This is now available to preview in 1.2.0-canary.0, which you can install using npm install tailwindcss@canary or yarn add tailwindcss@canary.

@hacknug
Copy link
Contributor

hacknug commented Oct 15, 2019

npx tailwind init works fine for me with or without the --full flag when installed on a project. Installing things globablly never sounds like a good idea to me. Maybe that's why your gridsome plugin wasn't working for me? 🤔

@hacknug
Copy link
Contributor

hacknug commented Oct 15, 2019

Issue must be coming from #1072 like I mentioned on Discord. I guess this is the line causing the issue. That's one of the reasons I don't like npm and yarn, pnpm would've probably catched that the moment that PR was opened if that's the actual issue 😃

@adamwathan
Copy link
Member Author

Can you create a GitHub repo similar to your project that I can use to test performance and try and solve it?

@adamwathan
Copy link
Member Author

Also, I may be misunderstanding the importing of configs, but I'm trying to add screens to the config. Adding them in an object exporting a config object directly in tailwind.config.js works, but not from within the bundling plugin.

Can you show me some example code so I can make sure I'm understanding correctly?

This should work for example:

// tailwind.config.js
module.exports = {
  plugins: [
    {
      config: {
        theme: {
          extend: {
            screens: {
              xxl: '1440px',
            }
          }
        }
      }
    }
  ]
}

@brandonpittman
Copy link

brandonpittman commented Oct 15, 2019

Here's the plugin content:

module.exports = {
  config: {
    theme: {
      extend: {
        screens: {
          dark: {raw: '(prefers-color-scheme: dark)'},
          light: {raw: '(prefers-color-scheme: light)'},
        },
      }
    }
  }
}

In tailwind.config.js, if I require it in the plugins key, the dark and light screens get added. If I require it from inside the bundling plugin, they don't.

Bundling plugin looks like this:

const postcss = require('postcss')

module.exports = function ({addComponents, addUtilities, addVariant, theme, variants, e}) {
  require("tailwindcss-plugin-prefers-color-scheme");
  require("tailwindcss-plugin-animated")({addUtilities, addComponents, e, theme, variants});
  require("tailwindcss-plugin-transitions")({addUtilities, variants});
  require("tailwindcss-plugin-content")({addComponents, addUtilities, addVariant, e});
  require("tailwindcss-plugin-aspect")({addUtilities, variants});
  require("tailwindcss-plugin-decoration")({addUtilities, variants, theme});
  require("@tailwindcss/custom-forms")({addUtilities, addComponents, theme, postcss})
}

The use of postcss as shown above will make canary hang, so I had to comment those lines out in testing.

@hacknug
Copy link
Contributor

hacknug commented Oct 15, 2019

Not sure why you need to import postcss there, it's not mentioned in the docs. Also, that's available in the object the plugins get so you could destructure in case you were going to need it (doubt it).

Have you tried turning this into a Tailwind plugin and use config.plugins[] to have all that together in your package?

@adamwathan
Copy link
Member Author

adamwathan commented Oct 15, 2019

If postcss is in the bag of crap each plugin gets, what's the point of this snippet requiring it in the docs?

Docs were written before that was added to the BOC™.

@brandonpittman
Copy link

Can we officially get it named bagOfCrap? 😀

@brandonpittman
Copy link

OK, here's the magic formula…

module.exports = {
  ...require("tailwindcss-plugin-prefers-color-scheme"),
  handler(bagOfCrap) {
    require("tailwindcss-plugin-animated")(bagOfCrap)
    require("tailwindcss-plugin-transitions")(bagOfCrap)
    require("tailwindcss-plugin-content")(bagOfCrap)
    require("tailwindcss-plugin-aspect")(bagOfCrap)
    require("tailwindcss-plugin-decoration")(bagOfCrap)
    require("@tailwindcss/custom-forms")(bagOfCrap)
  }
}

If you're using the object versions in tailwind.config.js, you don't need to care how they come, but if jamming everything into the bag of gimmicks like I am, you'll need to know which plugins you'll have to spread out.

The above totally works, perf not bad. I'm satisfied with this.

@eduarguz
Copy link

eduarguz commented Oct 15, 2019

I have been testing this new feature in an existing app and everything is working fine but with a notable performance issue.

tried to demo the issues in this repository, master branch uses v1.2.0-canary.0 and v1.1.2 branch uses the v1.1.2 version of tailwindcss

The performance issue can be seen when adding the @tailwindcss/custom-forms plugin

The resume:

tailwindcss v1.2.0-canary.0

  • without plugins
    • npm run dev Compiled successfully in 6737ms
  • with custom forms plugin
    • npm run dev Compiled successfully in 21241ms
    • that is 3.15x times

tailwindcss v1.1.2

  • without plugins
    • npm run dev Compiled successfully in 6247ms
  • with custom forms plugin
    • npm run dev Compiled successfully in 6177ms
    • there are not notable differences

@brandonpittman
Copy link

I can second those slower numbers. The slowdown from each successive plugin is definitely worse than before.

@adamwathan
Copy link
Member Author

@brandonpittman @eduarguz

Just tagged a new canary release that should fix this:

#1179

Grab v1.2.0-canary.1 and test if you don't mind! Thanks.

@eduarguz
Copy link

Awesome

Looks fine to me, some stats:

Dummy app

Using @latest

  • No plugins - Compiled successfully in 7587ms
  • Custom Forms Plugin - Compiled successfully in 6329ms

Using @1.2.0-canary.0

  • No plugins - Compiled successfully in 6656ms
  • Custom Forms Plugin - Compiled successfully in 18777ms

Using @1.2.0-canary.1

  • No plugins - Compiled successfully in 6425ms
  • Custom Forms Plugin - Compiled successfully in 6776ms 👍🏻

My app

Using @1.2.0-canary.0

  • Custom Plugins - Compiled successfully in 137323ms

Using @1.2.0-canary.1

  • Custom Plugins - Compiled successfully in 18091ms

Thank you so much Adam!

@brandonpittman
Copy link

Just wondering if 1.2 is ever coming out, or the updates here won't be seen until 2.0 in February.

Also, if you try to use @canary without a full-on build pipeline (just npx-ing it, even when installed locally), it can't find the resolve module. Installing that module from npm lets you use npx tailwind build though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants