postcss / postcss-load-config

Autoload Config for PostCSS
MIT License
635 stars 70 forks source link

`require('postcss-plugin')()` vs `require('postcss-plugin')` (without invocation) #225

Open andreavaccari opened 2 years ago

andreavaccari commented 2 years ago

Hi @ai, thank you for your work on postcss and autoprefixer (and your other packages). We're using them in combination with Vite, Svelte, and Tailwind, and we'd appreciate a clarification on how to specify plugins in postcss.config.cjs.

In this repo (ref), you instruct to require and then invoke a plugin:

module.exports = ({ env }) => ({
  plugins: [
    env === 'production' ? require('postcss-plugin')() : false    // <- with invocation
  ]
})

In autoprefixer (ref), you instruct to require a plugin without invoking it:

module.exports = {
  plugins: [
    require('autoprefixer')                                       // <- without invocation
  ]
}

While investigating performance issues with VS Code, I added a console.log to tailwind.config.cjs and found that:

Question: Could you clarify what is the preferred way to specify plugins?

Thank you for your help!

ai commented 2 years ago

Both ways are valid in PostCSS. The logic is:

  1. If you have no options, you can reduce () by writing only require('autoprefixer')
  2. If you have options, then you pass it to () require('autoprefixer')({ option: 1 })

PostCSS will detect, that () was not called and will call it automatically https://github.com/postcss/postcss/blob/main/lib/postcss.d.ts#L215-L216

andreavaccari commented 2 years ago

Thank you for the quick reply! I assumed this was the intended behavior.

Could I ask you to reproduce the following simple set up?

npm init --yes @svelte-add/kit@latest -- --with tailwindcss test
cd test
sed -i "" "1s/^/console.log('inside tailwind config')\n/" tailwind.config.cjs
npm run dev -- --open

At this point you should see the string inside tailwind config printed twice to console.

Now please kill the dev server and repeat the following steps.

sed -i "" "s/tailwindcss()/tailwindcss/" postcss.config.cjs
npm run dev -- --open

Now you should see the string inside tailwind config printed several times to console.

Do you have any insight into why this might be happening?

ai commented 2 years ago

I got this error during npm init call.

Can you show me the PostCSS config? Can you show me how do you load this config?

➜ npm init --yes @svelte-add/kit@latest -- --with tailwindcss test
➕ Svelte Add's SvelteKit app initializer (Version 2021.10.02.00)
file:///home/ai/.npm/_npx/9320fb454a84dc6d/node_modules/@svelte-add/create-kit/__init.js:33
        if (code !== 0) throw new Error(body);
                        ^

Error: error An unexpected error occurred: "Can't answer a question unless a user TTY".

    at ChildProcess.<anonymous> (file:///home/ai/.npm/_npx/9320fb454a84dc6d/node_modules/@svelte-add/create-kit/__init.js:33:25)
    at ChildProcess.emit (node:events:390:28)
    at maybeClose (node:internal/child_process:1064:16)
    at Socket.<anonymous> (node:internal/child_process:450:11)
    at Socket.emit (node:events:390:28)
    at Pipe.<anonymous> (node:net:672:12)
andreavaccari commented 2 years ago

The error you get seems related to yarn (ref). The same command should work with npm (ref).

To answer your question, the command generates this default configuration:

const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const cssnano = require("cssnano");

const mode = process.env.NODE_ENV;
const dev = mode === "development";

const config = {
    plugins: [
        //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
        tailwindcss(),
        //But others, like autoprefixer, need to run after,
        autoprefixer(),
        !dev && cssnano({
            preset: "default",
        })
    ],
};

module.exports = config;
const config = {
  mode: "jit",
  purge: ["./src/**/*.{html,js,svelte,ts}"],

  theme: {
    extend: {}
  },

  plugins: []
};

module.exports = config;
ai commented 2 years ago

The error you get seems related to yarn (ref). The same command should work with npm (ref).

I used npm init there. Maybe script found yarn in my system and called it?

How I can fix this issue to call code reproducion?

To answer your question, the command generates this default configuration:

Another questions:

  1. How inside tailwind config double call affect end-users?
  2. Where do you print this line?
andreavaccari commented 2 years ago

I create a simple repo so you can avoid the npm init problem:

https://github.com/andreavaccari/postcss-load-config-tailwind

Could you try the following:

You should be able to see that in the second case the tailwind.config.cjs file is loaded several times.

I'm investigating this issue because I have a monorepo with a sluggish DX and I'm trying to figure out if this is part of the issue.

ai commented 2 years ago

Is it just a performance problem of file multiple loading or it causes some bug? (Don't worry I will not ignore performance issue 😅)

andreavaccari commented 2 years ago

I believe it's only a performance issue. Thank you for being so responsive! 👍

WilhelmYakunin commented 2 years ago

Hello,

the scope of calling tailwindcss should be the tailwind-config object itself. Like here:

`console.log(">>> Loading postcss.config.cjs");

const tailwindConfig = require('./tailwind.config.cjs');
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const cssnano = require("cssnano");
const mode = process.env.NODE_ENV;
const dev = mode === "development";

const config = {
  plugins: [
    tailwindcss(tailwindConfig),
    autoprefixer(),
    !dev &&
      cssnano({
        preset: "default",
      }),
  ],
};

module.exports = config;
console.log(">>> Loading postcss.config.cjs");

I suppose that then tailwindcss called without confing in its scope so the default behaviour of tailwinds is to load the tailwindcss-confing for all its plugins separately. And that's more, the config file is finded by the tailwinds themself.

Do you mind if I pull the request?

andreavaccari commented 2 years ago

Hi @ai, it looks like @WilhelmYakunin is on the right track here. How would you like to proceed?

WilhelmYakunin commented 2 years ago

Hi, @andreavaccari, maybe I can evaporate the question you posed.

The official docs of Tailwindcss prescribe that by default Tailwindcss will look for a tailwind.config.js in the root of a project (please visit here).

Also, postCSS and Tailwindcss work together with having no search for config if the developer clearly puts the config (as an object) or path to config (as string) into tailwindcss function as the first argument. Just read the code of tailwindcss function that is taken from ‘.\node_modules\tailwindcss\lib\index.js’:

module.exports = function tailwindcss(config) {
  const resolvedConfigPath = resolveConfigPath(config);
  const getConfig = getConfigFunction(resolvedConfigPath || config);
  const mode = _lodash.default.get(getConfig(), 'mode', 'aot');

  if (mode === 'jit') {
    return {
      postcssPlugin: 'tailwindcss',
      plugins: (0, _jit.default)(config)
    };
  }

  const plugins = [];

  if (!_lodash.default.isUndefined(resolvedConfigPath)) {
    plugins.push((0, _registerConfigAsDependency.default)(resolvedConfigPath));
  }

  return {
    postcssPlugin: 'tailwindcss',
    plugins: [...plugins, (0, _processTailwindFeatures.default)(getConfig), _formatCSS.default]
  };
};

module.exports.postCSS = true;

But what happens when tailwindcss runs with and without invocation?

1st or with the invocation of tailwindcss in postCSS.config.js

The tailwindcss after invocation is the object, which has the property ‘postCSSplugin’. With the invocation of this object (as it is commonly known in JS functions are objects) a config is ‘undefined’ so the tailwindcss searches into the root folder and finds the config. In this case, the config is loaded once because the function calls only once. Why once? See the second;

2nd or WITHOUT the invocation of tailwindcss in postCSS.config.js

When the tailwindcss is written without invocation it is a function (and at the same time a first-class object) but has no property ‘postCSSplugin’. The property ‘postCSSplugin’ will reveal after invocation. So postCSS processor runs it as not an object (case ref) but as a function (case ref). And to process input CSS the postCSS processor needs to run (invocate) tailwindcss as a function. Afterwards, tailwindcss searches for a config because it did not pass into his first arguments (it is the default of tailwindcss). Actually, the posctCSS processor uses the tailwindcss plugin to process not only one css code but also: the code here here and so on. So in the second case, every time postCSS processor works with each of the input CSS it invokes the tailwindcss as the function which searches for config and loads it for every time of its invocation.


Postscript. Maybe I should also add.

If the project root folder wouldn't contain a file which name matches to substring 'tailwindcss-config' (with some of its variations) the tailwindcss will load the defaults config of its own (from stubs folder in tailwindcss node modules, alike here ones ref)

andreavaccari commented 2 years ago

Hi @WilhelmYakunin, thank you so much for taking the time to write this detailed explanation.

I now always invoke tailwind in the config file to avoid the subtly magical behavior of the alternative.