Skip to content

Conversation

@jquense
Copy link

@jquense jquense commented May 13, 2020

Notable Changes

Adds a utility for tools to customize and build their own postcss-loader's based on specific needs without disrupting the core logic of the plugin. The API was taken from babel-loader where it works well https://github.com/babel/babel-loader#customized-loader

Plan on using this in the Gatsby CSS tooling as well as other places

NOTE THAT THIS INCLUDES #425

Type

ℹ️ What type of changes does your code introduce?

  • CI
  • Fix
  • Perf
  • Docs
  • Test
  • Style
  • Build
  • Chore
  • Feature
  • Refactor

SemVer

ℹ️ What changes to the current semver range does your PR introduce?

  • Fix (:label: Patch)
  • Feature (:label: Minor)
  • Breaking Change (:label: Major)

Checklist

ℹ️ You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. This is a reminder of what we are going to look for before merging your code.

  • Lint and unit tests pass with my changes
  • I have added tests that prove my fix is effective/works (if needed)
  • I have added necessary documentation (if appropriate)
  • Any dependent changes are merged and published in downstream modules

Copy link
Member

@michael-ciniawsky michael-ciniawsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm absolutely -1 on this as I don't see the use case or benefit this change adds at the moment. What use cases does this enable which aren't currently possible? Please also give some examples if such use cases exists, maybe there is a another way to support them or I simply don't understand it as a Loader as Loader Builder feels, in all honesty, a bit awkward in my current opinion...

@alexander-akait
Copy link
Member

/cc @ai What do you think about it? I don't think what we should ignore a feature request from gatsby

@jquense
Copy link
Author

jquense commented May 14, 2020

fair questions. It enables creating higher-level loaders on top of postcss without having to copy/paste the postcss-loader. Right now there isn't a good way for tools to both provide postcss based transforms as well as letting downstream users customize for other things.

For instance Gatsby's css pipeline defaults to autoprefixing and handling some flexbugs for CSS here. This works fine but removes the ability for a user to further configure postcss with a config file, and requires that awkward function wrapping to support it in loader options. To work around this, the gatsby-plugin-postcss has to introspect the webpack config rules for the css rule, remove it, and add it's own with an un-optioned postcss-loader, which also removes autoprefixing by default, and requires the user set it up (which also means needing to setup browserslist and opting out of Gatsby's default handling of that).

Alternatively this PR allows higher level tooling to produce and use slightly modified postcss-loader with preset's and option handling that are tuned to the more convention based environment the user's code is running in. Such as configuring TailwindCSS: https://github.com/jquense/docpocalypse/blob/master/packages/theme/tools/theme-loader.js without breaking postcss.config.js behavior

Yes this could be done a few other ways, like using multiple postcss-loaders in a loader setup, but that is slower (even with #425) and not as flexible, because you have far less control over plugin execution order, as well as not being able to set the parser/stringifier effectively for all user code.

This could also be accomplished via some additional options in postcss-loader, (like letting config be a function that is passed the options/file config and can mutate them). That works less well for tools wrapping it, since it doesn't allow providing a separate smaller/different API on top of the postcss-loader. e.g. my-postcss-loader may not allow configuring the parser b/c my tool would break if not the default.

I also find this API a bit MEH, but there is precedent for it in babel, and it's not intended as an API for direct (most) users of the loader, so maybe it's less important the API be pristine (YMMV!).

For reference here is Gatsby's usage of the same feature from babel-loader)

Also just to be clear I'm not here with an official request from Gatsby, tho I did write/maintain a bunch of their CSS stuff, and do plan on upstreaming it there. And also would benefit from it in Gatsby, as well as in other projects I maintain like astroturf

const semver = require('semver')
const loader = require('../src')

const incomingVersion = semver.inc(postcssPkg.version, 'minor')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can take PostCSS version by postcss().version or by result.processor.version in PostCSS plugin API

@@ -0,0 +1,248 @@
const path = require('path')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to keep old file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't forgot to remove it!

@ai
Copy link
Contributor

ai commented May 14, 2020

It is hard to review big API changes without code and use cases. I see the problem and agree that we must provide some solutions for users with this problem. But I am not sure that it is the best way (not because I know a better way, but because I do not fully understand the problem).

Can we start from a little early and create an issue, where we will describe the problem and provide more use cases?

Sorry, @jquense I know that it means more paperwork for you. But I afraid to take the wrong decision, reduce project maintainability, and bring to many API changes to PostCSS users (if we will need to fix API in the future).

@jquense
Copy link
Author

jquense commented May 14, 2020

I'm happy to do the work to describe more, but I do want to try and protect myself from unnecessary work a bit.

  • This doesn't change the API for any user of postcss-loader
  • This is not going to change how 90% of ppl use the postcss-loader and those who do use it are less likely to be a maintenance burden being tool authors themselves
  • The API is a bit ugly but at least proven effective in babel (obviously a different tool but in the same ballpark so maybe we can take lessons from it)
  • This project hasn't add an update or release in 2 years, so I don't think API churn or maintenance burden is big risk here (that may be an unfair assumption), it's basically already on autopilot (NOT a criticism, I wish all my projects were like that)

I think there is relatively little risk, all existing tests pass, no breaking API changes, and if something goes wrong, it's easy to revert and bump.

@jquense
Copy link
Author

jquense commented May 14, 2020

it should also be said that i'm happy to help maintain as well if anyone wants to/trusts me with a commit bit :P

@ai
Copy link
Contributor

ai commented May 14, 2020

it's basically already on autopilot

Accepting some big PR could stopped autopilot 😅. People will create a lot of issues and force us to spend more time.

@jquense
Copy link
Author

jquense commented May 14, 2020

that's fair and true! I'm willing to help if that happens. Again i don't mean that as a negative.

I also think that's a benefit to just stealing what babel did.

What would helpful to describe in addition to my use-case description here #428 (comment)

@michael-ciniawsky
Copy link
Member

This is not going to change how 90% of ppl use the postcss-loader and those who do use it are less likely to be a maintenance burden being tool authors themselves

Yes but it needs to be documented and especially new users might be inclined to use it due to not understanding it's use correctly

The API is a bit ugly but at least proven effective in babel (obviously a different tool but in the same ballpark so maybe we can take lessons from it)

That is my main concern, maybe Gatsby needs this kind of customization, but why e.g not simply writing a new loader then instead of adding a 'Loader Builder' to this Loader. This also has the benefit of being able to maintain this loader freely then

This project hasn't add an update or release in 2 years, so I don't think API churn or maintenance burden is big risk here (that may be an unfair assumption), it's basically already on autopilot (NOT a criticism, I wish all my projects were like that)

I did a lot of testing around to ensure it is that way, there are simply no pressing updates needed atm and no new issues being opened in a while, it's a simple loader wrapping postcss for webpack usage in a 'generic' way, meaning you should be able to use all postcss related features and have as minimum webpack idiosyncrasies as possible

@ai
Copy link
Contributor

ai commented May 14, 2020

That is my main concern, maybe Gatsby needs this kind of customization, but why e.g not simply writing a new loader

Code reuse is a good thing. Sor instance, if we found and fix a bug in postcss-loader it will be nice to have a way for the Gatsby team to backport it faster.

Also, it will simplify PostCSS major updates.

@jquense
Copy link
Author

jquense commented May 14, 2020

Yes but it needs to be documented and especially new users might be inclined to use it due to not understanding it's use correctly

I'm happy to add a bit note saying most ppl won't/shouldn't use this if that helps. Also the API is fairly obtuse to use just accidentally since you need to create a new file, which i think is a positive.

but why e.g not simply writing a new loader

That is always an option, It's not great tho. The ecosystem is built on top of this loader and having other postcss based ones means there is room for underlying behavior forking. It's harder to ensure that compatible versions of postcss are used together, or that things like AST reuse work between tools, or that things like config loading are consistently handled.

This isn't a big loader but there is choices made there that affect up and downstream users. Consider that css-loader tries to reuse the AST from postcss, if others make there own loader they have to remember to do that as well, maintain the same format etc as postcss-loader.

Is it huge deal if these tools made their own loaders? No, but it's also not necessary and yes the cost it borne by maintainers here, and that's a thing to consider.

I did a lot of testing around to ensure it is that way

You did great work! not all projects need to update constantly, it wasn't a negative, just noting that there may be room for some changes now, and that users probably won't mind/notice given how infrequently they already have to think about this loader 👍

@michael-ciniawsky
Copy link
Member

michael-ciniawsky commented May 14, 2020

I'm happy to add a bit note saying most ppl won't/shouldn't use this if that helps. Also the API is fairly obtuse to use just accidentally since you need to create a new file, which i think is a positive.

Right, I'm ok with this being 'hidden' for implementers (normally more advanced folks in the tooling space) in e.g ./docs/IMPLEMENTERS.md or the like

That is always an option, It's not great tho. The ecosystem is built on top of this loader and having other postcss based ones means there is room for underlying behavior forking. It's harder to ensure that compatible versions of postcss are used together, or that things like AST reuse work between tools, or that things like config loading are consistently handle

Agreed, there might be a way to handle this as setting defaults is not only a problem for gatsby in particular but also for wrappers like create-react-app etc at the moment, due to how config loading works. I will look into it in the next days as I'm currently not happy with how it is done to be honest. Reusing AST's is each loaders concern there is now way around it, it's a essentially a webpack feature to pass them between loaders and in this case config loading is abstracted into it's own package aswell (see the next paragraph below). So e.g something in the direction of e.g maybe sufficient, but I need to look at it more throughly first....

loader.js

function loader () {
 // ...

 if (loader.defaults) {
   // Handle defaults
 }

 // ...
}

loader.defaults = false

module.exports = loader

custom-loader.js

const loader = require('./loader')

// Applied after options or config loading
// to be usable as the 'normal' loader
// but with specific defaults
loader.defaults = {
  plugins: []
  options: {}
  ...etc
}

module.exports = loader

webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [{
      // ...
      loader: path.join(__dirname, './custom-loader.js'),
      options: {
         // Used as always...
      }
    }]
  }
};

Is it huge deal if these tools made their own loaders? No, but it's also not necessary and yes the cost it borne by maintainers here, and that's a thing to consider.

And at some point it is the better choice imho depending a how much customization is needed, the way it is currently proposed is able to basically 'hook' into everthing, which 1.) shouldn't be needed and 2.) makes reusing the code a bit senseless as it doesn't seem to provide the needed logic in general then.

You did great work! not all projects need to update constantly, it wasn't a negative, just noting that there may be room for some changes now, and that users probably won't mind/notice given how infrequently they already have to think about this loader 👍

I didn't recieve it as a negative, but to explain why it turned out to be likely be this way.

@michael-ciniawsky
Copy link
Member

I will open a tracking issue once I have some more context and thought about it

@michael-ciniawsky
Copy link
Member

So essentially the use case is fine, but I want to have some gurads of how one can customize it to avoid getting issues which regularly force me to ask "show your custom loader file" or the like. Adding e.g a few default plugins and options with a merge of the user options or config provided are fine for example as there shouldn't be to much 'abuse' potential if done right in a certain way still handled by the 'original' loader (Errors, Warnings, ...).

@jquense
Copy link
Author

jquense commented May 15, 2020

I would add if we think the scope of this API is to much it could be easily pulled back to the minimal i think is necessary for these use-cases

  • handle new loader options or filter/map postcss-loader ones
  • A hook to modify/introspect the config for each file processed

Without proposing a very different API that would look like

  • removing the result hook (use a postcss plugin if needed!)
  • remove a bunch of the flavor options passed to customOptions and config hooks

@ai
Copy link
Contributor

ai commented May 15, 2020

Nope, I think we are going in the right direction. I just want to increase paperwork for the first planning step.

What is the problem exactly? What is the current hacky solution (with code examples)? What are the limits of the current solutions? What are the possible alternatives? What other problems we can potentially fix?

Unfortunately, bureaucracy is really useful thing for the big system (I spend 5 years as a Wikipedia administrator and can be sure about it 😅).

@jquense
Copy link
Author

jquense commented May 15, 2020

@ai I think i did that here: #428 (comment) statement of the problem, workarounds and alternatives. Is there something missing you'd want to see

@ai
Copy link
Contributor

ai commented May 15, 2020

So, do you want an extendable postcss-loader?

You define default PostCSS plugins, but they can be overridden in user’ config?

Few thoughts:

  1. Do we need to allow users to disable/change some plugins from the library config? For instance, if I want to pass grid: 'autoplace' option to Autoprefixer, but the library has Autoprefixer in its PostCSS config?
  2. Instead of changing postcss-loader we can change postcss-load-plugins and add this feature to Parcel and all other builders. postcss-loader supports standalone PostCSS config. But I am not sure about the API.

@jquense
Copy link
Author

jquense commented May 15, 2020

Do we need to allow users to disable/change some plugins from the library config?

I don't think so, There are two main things i think need to be possible:

  1. the ability to limit or augment user provided config, so filtering out things that would break in the specific tooling context
  2. and ability to set configuration that is NOT overridable by users except through explicit API of my custom loader

As an example I may want to allow users to disable Gatsby's built in autoprefixing behavior, but not arbitrarily edit my postcss config. In general I'm looking for the ability to provide higher level interfaces to users, maybe even to the extent of abstracting away Postcss as a concept they need to know. As opposed treating the user as "peer" which is mostly what happens now.

@ai
Copy link
Contributor

ai commented May 17, 2020

Another question: do we want to put plugins at the beginning of the config? Or we will add user’s plugins to the end of the list? On the one hand, PostCSS 8 will be less important. In another hand, even in Babel (they have visitor API which we will add to PostCSS 8) plugin’s order sometime it is important.

What will be the logic behind this PostCSS config merging?

@jquense
Copy link
Author

jquense commented May 17, 2020

I don't think generic config merging is the feature needed here at least it's not the one I'm looking for for this use case. One size size wouldn't fit all the cases and don't address the use case of easily changing the config based on the current file well, amount others. Plugin ordering is a great example, some plugins would need to be first and others, like autoprefixer, last. The flexibility of a function where the tooling can provide their own logic is needed

@ai
Copy link
Contributor

ai commented May 17, 2020

@jquense yeap, I agree that custom callback is an interesting solution here.

We can add postcssLoadPlugin.extend(config => changedConfig) and add extend option to the loader. In this case, we will be able to re-use config extendable in all bundler, it will be not a webpack-specific solution.

@ai ai closed this May 17, 2020
@ai ai reopened this May 17, 2020
@jquense
Copy link
Author

jquense commented May 18, 2020

That seems ok, but how would handle accepting new loader options? You need some way of split out my-custom-loader options from the base postcss-loader options before they are validated, otherwise it'll fail the JSON schema here

@ai
Copy link
Contributor

ai commented May 18, 2020

That seems ok, but how would handle accepting new loader options?

I think we can add options to the loader and then just pass it to the postcss-load-plugins library.

What options do you need?

@jquense
Copy link
Author

jquense commented May 18, 2020

The concern is more about validate, postcss-loader whitelists the accepted options you can pass it. you can't pass additional options (like ones to do additional configuration) without it yelling at you: https://github.com/postcss/postcss-loader/pull/428/files#diff-1fdf421c05c1140f6d71444ea2b27638R74

just noticed there is a bug here, the customOptions hooks should run before validate

@ai
Copy link
Contributor

ai commented May 18, 2020

And we can always add new options from postcss-load-plugins to postcss-liader whitelist

@alexander-akait
Copy link
Member

@jquense friendly ping, do you still need this feature?

@jquense
Copy link
Author

jquense commented Sep 8, 2020

I still think this is a good idea and one that i would use. At the moment i've forked postcss-loader to accomplish this. Open to different or simpler API's but I think having a public API for tooling authors to build extensions on postcss-loader would be a win.

@alexander-akait
Copy link
Member

@jquense Can you provide me simple use cases for this feature? I would like to evaluate this or perhaps suggest other approaches, for example you can use postcssLoader.call() to run postcss-loader inside own loader

@jquense
Copy link
Author

jquense commented Sep 8, 2020

@evilebottnawi #428 (comment) I think describes the use cases and #428 (comment) the important requirements.

I would add again that this API has already been tested and proven out for Babel, which has mostly the same concerns and use cases for tooling authors. Happy to bikeshed simpler alternatives but also don't want to reinvent the wheel for the sake of it.

postcssLoader.call isn't a good option as it requires duplicating a lot of config reading logic. Any API should make it easy to reuse all the specific logic the core loader does while also giving the consumer control over the options passed in as well as the final set of options and plugins passed to postcss.

@alexander-akait
Copy link
Member

@jquense I would like to see examples of usage (minimum), based on the logic described, we will need to create such logic for almost every loader, I find it a little redundant and would like to understand the problem deeper

@jquense
Copy link
Author

jquense commented Sep 8, 2020

sure, here is one example of me using it to preconfigure autoprefixer for using postcss in a chain with other preprocessing https://github.com/jquense/docpocalypse/blob/master/packages/gatsby-plugin-css/utility-postcss-loader.js (css-loader!postcss-loader!sass-loader for instance)

I also use it here as well to configure tailwindcss in a specific way so that it works out of the box with the gatsby theme: https://github.com/jquense/docpocalypse/blob/master/packages/theme/tools/theme-loader.js

@alexander-akait
Copy link
Member

You can achieve this using https://github.com/webpack-contrib/postcss-loader#function I still can't understand use case

@jquense
Copy link
Author

jquense commented Sep 10, 2020

You can achieve this using https://github.com/webpack-contrib/postcss-loader#function

That is not quite correct.

  • The function options does not let you adjust or map the options from a user's config file.
  • providing postcssOptions turns off config handling recently added i see 👍
  • There is no way to abstract it away from a user and rely, i cannot easily provide a "my-theme-loader" to a user in a way that they can't mess up, there a ton of extra work needed to achieve this with the current setup

You have to do something like this ugliness which doesn't even really work:

function myThemeLoader(src) {
   // duplicated from postcss-loader
   const config = readPostCssConfig(this.resourcePath)
   
   return postcssLoader.call({
     ...this,
     query: {
       postCssOptions() {
          return doStuff()
       }
     } 
   }, ...args)
}

@alexander-akait
Copy link
Member

The function options does not let you adjust or map the options from a user's config file.

Why you need this?

There is no way to abstract it away from a user and rely, i cannot easily provide a "my-theme-loader" to a user in a way that they can't mess up, there a ton of extra work needed to achieve this with the current setup

Because it was never designed for this purpose

I cannot understand the purpose of this.

You have to do something like this ugliness which doesn't even really work:

This is a perfectly good solution.

@ai
Copy link
Contributor

ai commented Sep 12, 2020

I think that similar feature in babel-loader is enough reasons for implementing it here if we had no some other reasons to not do it.

@jquense
Copy link
Author

jquense commented Sep 13, 2020

I'd also add that I've had lots of problems trying to extend webpack loaders by calling another loader in it. There are many idiosyncracies to spreading the webpack loader context. Like how some properties are recent or nulled out later making things break subtly in hard to diagnose ways. Not to mention caching issues and the like. Overall webpack loader pipeline is not designed for that sort of composition

@alexander-akait
Copy link
Member

@ai The original design did not design for such a possibility, the official introduction of this will potentially create a situation where we will have to do this for each loader, then we need to completely revise the design to avoid reimplementation this for each loader

@ai
Copy link
Contributor

ai commented Sep 15, 2020

@evilebottnawi I understand that is hard. But making thing hard should not be a reason to not do it.

If users need some feature, we need to help it (when we will have a time). If they want to spend their time in fixing the issue, how the fact that it isard can be a blocker for them?

The good reason is that will increase maintaibility. Or that the current implementation is not the best one.

@alexander-akait
Copy link
Member

@ai

I understand that is hard. But making thing hard should not be a reason to not do it.

This is why I have not closed this issue/PR 😄

I'm just saying that maybe we should look differently at the implementation.

@jquense Can you rebase your work?

@jquense
Copy link
Author

jquense commented Sep 22, 2020

i will rebase! i'm quite busy with work at the moment but will get back to this

@jquense jquense closed this May 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants