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

why async? #68

Closed
stevenvachon opened this issue Sep 27, 2015 · 37 comments
Closed

why async? #68

stevenvachon opened this issue Sep 27, 2015 · 37 comments
Milestone

Comments

@stevenvachon
Copy link
Contributor

postcss([ cssnano ]).process(styles).css
// or
cssnano.process(styles).css;
Error: Use process(css).then(cb) to work with async plugins

I'd much prefer to use postcss synchronously in my current project.

@TrySound
Copy link
Collaborator

@stevenvachon postcss is used only asynchronously. Sync mode exists only for debug. Also you get this warning cause one of the plugins is async and couldn't be used synchronously.

@stevenvachon
Copy link
Contributor Author

Ok, thanks!

@SassNinja
Copy link

@TrySound is there a specific reason to not support the sync API of PostCSS?
It has a right to exist imo.

I'm currently working on an own plugin (postcss-extract-media-query) where I'm forced to do synchronous calls (because my extraction is not part of the bundler flow).

My solution is using csswring which works without problem. But I'd prefer to use cssnano as it is more supported.

@TrySound
Copy link
Collaborator

/cc @evilebottnawi

@alexander-akait
Copy link
Member

@SassNinja some packages what we use in cssnano work only in async mode, sorry we can't do here anything

@ai
Copy link
Member

ai commented May 29, 2018

If I remember correctly we added async just for postcss-svg. Maybe there is a option to keep sync API.

@alexander-akait
Copy link
Member

Anyway right now we freeze new feature(s), let's open this and maybe do this in 4.1 version

@alexander-akait alexander-akait added this to the 4.1 Next Feature Release milestone May 29, 2018
@TrySound
Copy link
Collaborator

TrySound commented May 29, 2018

because my extraction is not part of the bundler flow

What does it mean? You can do async js everywhere. If you don't use require extensions hook I don't see any reason for sync api and make things more complex and slower.

@SassNinja
Copy link

Ok got it, one/some packages are the wrongdoer. But if @ai is right and it can be fixed in future version, it would be awesome!

What does it mean? You can do async js everywhere. If you don't use require extensions hook I don't see any reason for sync api and make things more complex and slower.

@TrySound I'm afraid that's not possible in my case (would be happy if I'm wrong).
The problem with webpack (I've primary written my plugin for this bundler) is it can't split CSS into chunks. Thus I can't emit the extracted files with webpack but have to do this within my plugin via the node filesystem (fs-extra).

If I do this async I'll take the risk of race conditions (webpack thinks postcss is done and continues while the plugin might still be running).
Feel free to check my code and let me know if you think I'm wrong.

@alexander-akait
Copy link
Member

alexander-akait commented May 29, 2018

@SassNinja Some notes for webpack

  1. use compiler.outputFileSystem instead fs (otherwise you plugin will be very slow in watch mode).
  2. webpack can emit files 😕

Related webpack-contrib/postcss-loader#354?

About code:

  • Looks queries should be Map/WeakMap.
  • Why you need combine option?
  • Remove minimize option, it is out of scope plugin, one plugin - one goal (if somebody want minimize it will be use minimize plugin)
  • For stats better use https://github.com/postcss/postcss-reporter

@alexander-akait
Copy link
Member

You mix logic postcss and webpack, it is very bad practice, just write postcss plugin what get one css file and output two/three/more based on count of media queries. When ping me and I'll show you how it is use in webpack.

@SassNinja
Copy link

@evilebottnawi thanks for the feedback! I've (again) some questions :)

use compiler.outputFileSystem instead fs (otherwise you plugin will be very slow in watch mode).

You're talking about the webpack compiler? Do you have a link to the appropiate section in the docs? I can't find anything about outputFileSystem here

webpack can emit files

I assume you're refering to webpack-contrib/postcss-loader#354 but as I understand the feature (this.emitFile) still needs to be added to the postcss-loader, doesn't it?

But talking about webpack: although I've built the plugin primary for weback it can (in theory - tests running) be used with other bundlers as well. I'm afraid that too much webpack dependency (compiler, postcss-loader) won't offer that much more benefits... or do you think it would be better nevertheless?

Looks queries should be Map/WeakMap

Do you literally mean a Map object? (I don't know the advantage in this case bcz a regular object suits quite well) Or are you just talking about renaming the option?

Why you need combine option?

My plugin does not only extract the media query, but also merge equal rules into one.
There'S also a plugin that does only the merge (css-mqpacker) but I didn't like the code and have prefered to create this myself.
Are you putting this feature in question or just the option (meaning it should always be true)?

Remove minimize option, it is out of scope plugin, one plugin - one goal (if somebody want minimize it will be use minimize plugin)

I first thought exactly the same. But the problem is I'm writing the extracted files within my plugin (fs.outputFileSync()). Therefore these files won't get minimized no matter if you use another plugin (such as cssnano) or not. That's why I included this option.

It's simply a problem here I'm not emiting the files with webpack. On the other hand I'd like to keep my plugin bundler independent (see above)...

For stats better use postcss/postcss-reporter

Thanks for the tip! I'll check this when I've got some time. But in the end it's only about a few console.log() - not sure if I need another package for this. I'll think about it.

You mix logic postcss and webpack, it is very bad practice, just write postcss plugin what get one css file and output two/three/more based on count of media queries. When ping me and I'll show you how it is use in webpack.

So perfect way in your eyes would be to send the CSS (example.css) with webpack / postcss-loader to my plugin, split within the plugin and send back to webpack to emit the files?

Even if possible (still not sure how to do - see questions above) this would mean 100% webpack dependency, wouldn't it?

@michael-ciniawsky
Copy link

const postcss = require('postcss')

const plugin = postcss.plugin('plugin-name', (options) => {
   return (css, result) => {
      const queries = []

      css.walkAtRules((rule) => {
          /* ...extract @media */
          queries.push({
              type: 'file',
              name: 'tablet',
              value: ...css
          })
      })
     
     result.messages = result.messages.concat(queries)
   }
})

module.exports = plugin
const postcss = require('postcss')

class CSSMediaQueryPlugin {
  constructor (options = {}) {
     this.plugin = { name: 'CSSMediaQueryPlugin' }
     this.options = options
  }

  apply (compiler) {
     const { plugin, options } = this
     const { compilation } = compiler.hooks
     
     compilation.tap(plugin, (compilation) => {
        const { assets } = compilation
        const { optimizeAssets } = compilation.hooks
        
        optimizeAssets.tapAsync(plugin, (assets, cb) => {
           Object.keys(assets)
             .filter((asset) => asset.endsWith('.css'))
             .forEach(async (asset) => {
                const src = assets[asset]
                
                options = Object.assign(options, { from: asset })

                const { css, messages } = await postcss([ /* your plugin */ ])
                  .process(src, options)

                messages
                  .filter((msg) => msg.type === 'file')
                  .map(({ name, value }) => {
                    const name = asset.replace(/\.css$/, `-${name}.css`)

                    compilation.assets[name] = value
                })
                // if only the queries get pruned from the original
                compilation.assets[asset] = css
                // if all css gets split up into chunks
                delete compilation.assets[asset]
              })

           cb()
        })
     })
  }
}

module.exports = CSSMediaQueryPlugin

⚠️ Requires e.g mini-css-extract-plugin to get the original CSS Chunk on compilation.assets

Code is obviously pseudo, untested and needs some polishing based on what you want in the end...

@SassNinja
Copy link

@michael-ciniawsky many thanks for your code example – looks quite good!
(sorry for the late reply – I've been on vacation)

Before having noticed it I did experiment with a custom webpack-loader solution but have finally given up that idea because it's always limit to the one chunk. No matter what I add to the code (@import, url()) in the end it's only one chunk meaning one file.

Afterwards I started to work on a plugin solution.
Similar to yours but I did use compiler.hooks.emit.tapAsync() instead of your compiler.hooks.compilation.tap().

Is there any advantage of using the compilation optimizeAssets hook?
(the api docs don't provide much information about the 'optimization')

Anyway I'll try to create a working plugin based on your and my code.
But I'm still facing a major problem: sourceMaps

The compiled CSS has one sourceMap (assets[asset].children[0]._value) which gets lost (inline sourceMap) during the postcss process where I extract the media atRules.
Even if I would preserve the original sourceMap it would only affect the original CSS file (which has been reduced) and the extracted files would be without any sourceMap.

Do you have an idea how to fix the sourceMap issue?
Maybe @evilebottnawi has an idea?

@SassNinja
Copy link

SassNinja commented Jun 14, 2018

I've created a repo with first draft:
https://github.com/SassNinja/split-css-plugin
Just execute npm test and check the folder test/output/.

I haven't found a solution for the sourceMap issue yet.
The map of the initial css chunk doesn't get changed and the new files don't have any map. Besides the way I read the css code feels a bit hacky src.children[0]._value

any help appreciated

@andyjansson
Copy link
Contributor

Can this be closed?

@alexander-akait
Copy link
Member

@andyjansson i think yes, some optimization impossible do sync

@strarsis
Copy link

So it isn't possible to use cssnano synchronously, right?

@alexander-akait
Copy link
Member

Why you need cssnano in sync mode?

@alexander-akait
Copy link
Member

alexander-akait commented Jul 11, 2019

svg/svgo#1127

@alexander-akait
Copy link
Member

We can implement this in next version(s), i will ping you

@strarsis
Copy link

strarsis commented Jul 11, 2019

@evilebottnawi: Correct, svgo plugin model is synchronous.
cssnano should be used instead of csso in the minifyStyles svgo plugin as
it is not maintained anymore.

@tomrav
Copy link

tomrav commented Jul 15, 2019

I too have a use-case that would really benefit from cssnano being available in sync mode. It would allow us to transition over from CleanCSS.

@strarsis
Copy link

Just encountered another issue with csso where it unintentionally removes pseudo-classes (like hover).

@10xjs
Copy link

10xjs commented Mar 8, 2020

It is possible to use the current version of cssnano synchronously with a little effort. cssnano itself is a modular set of postcss plugins, few of which is actually async. We are able to sidestep all of the async code by referencing the internal plugins directly (though one of the presets to save having to import all of the plugins directly).

import postcss from 'postcss';
import cssnanoPresetDefault from 'cssnano-preset-default';

const preset = cssnanoPresetDefault();

const asyncPlugins = ['postcss-svgo'];

const plugins = preset.plugins
  .map(([creator, pluginConfig]) => {
    // replicate the `initializePlugin` behavior from https://github.com/cssnano/cssnano/blob/a566cc5/packages/cssnano/src/index.js#L8
    return creator(pluginConfig);
  })
  .filter((plugin) => {
    return !asyncPlugins.includes(plugin.postcssPlugin);
  });

const result = postcss(plugins).process(css).toString();

@Siilwyn
Copy link
Contributor

Siilwyn commented Mar 9, 2020

With async/await now existing, this issue is solved for those who want to wait for the processing to finish with await. I don't think a sync alternative way to call cssnano is worth it.

@itsdouges
Copy link

thanks @10xjs it worked! for those interested i'm using this in a typescript transformer which doesn't currently have an async api (...that i've seen 😅)

@ludofischer
Copy link
Collaborator

As I see it, the issue is that the cssnano does not implement the process() method itself, it comes directly from PostCSS. And the PostCSS docs say the sync API is for debug purposes only: https://postcss.org/api/#lazyresult-css,

@ludofischer ludofischer modified the milestones: 4.1, backlog Mar 9, 2021
@ludofischer
Copy link
Collaborator

I think we can close this issue as the cssnano plugins are sync, it's PostCSS itself that as of PostCSS 8 does not have an (offical) sync API any more.

@ai
Copy link
Member

ai commented Nov 3, 2021

PostCSS 8 still have sync API (most of my plugins are sync).

@ludofischer
Copy link
Collaborator

PostCSS 8 still have sync API (most of my plugins are sync).

Is it officialy supported? The docs still say 'this method is only for debug purpose' https://postcss.org/api/#lazyresult-css

@ai
Copy link
Member

ai commented Nov 3, 2021

LazyResult#css is not a plugin API.

This recommendation is for runner’s developers (like postcss-cli or postcss-loader). They always should be ready for async plugin (so they must use await result).

Plugins still can use sync or async APIs depends on internal needs.

If you have a wrapper around the single plugin (so you sure that it is sync plugin), you can call .css.

Maybe I should add this note about wrappers also to docs.

@ai
Copy link
Member

ai commented Nov 5, 2021

I fixed docs postcss/postcss@7cccfac

@kiner-tang
Copy link

@ai Does the latest version of cssnano support the synchronization API?

@ai
Copy link
Member

ai commented May 12, 2023

@kiner-tang I am not cssnano maintainer

@ludofischer
Copy link
Collaborator

@kiner-tang I am not sure I understand the question. What are you trying to do?

@bennypowers
Copy link

@10xjs I tried your method and got Error: Use process(css).then(cb) to work with async plugins, so I tried the plugins in the list one by one and arrived at this config:

const isPlugin = (x: PostCSSPlugin|PostCSSProcessor): x is PostCSSPlugin => 'postcssPlugin' in x;

const cssnanoPreset = cssnanoPresetDefault();

const asyncPlugins = [
  'postcss-svgo',
  'css-declaration-sorter',
];

const cssnanoSyncPlugins = cssnanoPreset.plugins
  // replicate the `initializePlugin` behavior from https://github.com/cssnano/cssnano/blob/a566cc5/packages/cssnano/src/index.js#L8
  .map(([creator, pluginConfig]) => creator(pluginConfig))
  .filter(plugin => !isPlugin(plugin) || !asyncPlugins.includes(plugin.postcssPlugin));

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

No branches or pull requests