Skip to content

Custom module export support #645

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

Closed
ZeeCoder opened this issue Dec 13, 2017 · 16 comments
Closed

Custom module export support #645

ZeeCoder opened this issue Dec 13, 2017 · 16 comments

Comments

@ZeeCoder
Copy link

I'm not sure if I'm opening this issue at the right place, but I'm not certain where else could I address this issue.

My goal
I wanted to make a custom loader, that would allow me to import some statistics from a css file, like so:
import stats from 'MyComponent.css';
This setup would also involve style-loader and css-loader so that my css is injected in the browser.

The issue
It seems like due to the tight coupling of the style-loader and css-loader in module: true mode, I can't define custom exports like I wanted to.

I thought even the following would be possible:
import { loader1Data, loader2Data } from 'MyComponent.css';
Where those imports are whatever the respective chained loaders decided to expose.

Question
Would the above be possible somehow?
To me it seems like this would need a rewrite and some standard that loaders working on CSS would need to follow in order to contribute to a module that's finally exported at the end.

I'm really interested in any thought, or advice on who I need to approach with this problem.

Also, this is the original discussion on the webpack/dev Gitter:
https://gitter.im/webpack/webpack/dev?at=5a2c9ce93a80a84b5bdac9ef

@michael-ciniawsky
Copy link
Member

michael-ciniawsky commented Dec 13, 2017

It should be possible, but I need to triage the use case for custom CSS exports first before consider adding it to css-loader directly. Checkout #642 for how you can do this with PostCSS && Plugins.

css-meta-loader

import { getOptions } from 'loader-utils';

export default function loader (css, map) {
  // Loader Options
   const options = getOptions(this) || {};
  // Make the Loader Async
   const cb = this.async();
  // Some transform you need to get the metadata e.g via postcss && plugins
  ... (?)
  // Dummy
  const exports = `export const meta = 'Hello';`
 
  const result = [
     `// CSS Exports\n${exports}\n`
     `export default `\${css}\``,  
  ].join('\n')  

  cb(null, result, map);

  return null
}
import css, { meta } from './file.css'

What kind of stats (metadata) you intend to include and where does it come from/how is it generated ?

@ZeeCoder
Copy link
Author

That's how I thought it'd work.
The problem is that the style-loader that injects the styles actually looks for the css-loader's
locals specifically, and only exports that as default:
https://github.com/webpack-contrib/style-loader/blob/master/index.js#L26

Nothing else gets through.

I'm not actually sure if the css-loader itself respects exports made by previous loaders on the same source.
I might have to do more tests and write down the results. 😅

I have two use-cases currently, but I could think of more:

  1. I have a container-query solution (https://github.com/ZeeCoder/container-query), which uses a postcss plugin to extract container-query-specific declarations and put them in a JSON / JS object.
    Then that can be fed to the JS runtime. Currently the plugin just saves this JSON aside the original css, and the I require() that JSON to start the runtime.
    One of the problems with this approach though is that it makes webpack trigger a second built every time the CSS is changed. I would instead like to be able to do the postcss processing through a loader, then export the results. I would also want to support CSS-modules in the long term, so I should be able to chain my loader after the css-loader, operate on the same raw css, then export both css-module specific data (class mapping) and the extracted query stats.
  2. In my company we use BEM convention for all our components, but don't use css modules. Instead, we have a little BEM class, and we create one instance for each component.
    Basically it looks like this:
import 'MyComponent.less';
const bem = new BEM('MyCOmponent');

Instead, this, or something similar would be desirable:
import bem from 'MyComponent.less';

@michael-ciniawsky
Copy link
Member

michael-ciniawsky commented Dec 13, 2017

The problem is that the style-loader that injects the styles actually looks for the css-loader's
locals specifically, and only exports that as default:

Yep correct, this will change in style-loader >= v1.0.0, but how exactly isn't set in stone yet. Intended is

import * as styles from './file.css'

// CSS Modules
// from 'var locals = { container: '3467i2fgz'}' (CommonJS)
// to 'export const container = '3467i2fgz' ' (ESM Named Export)
console.log(style.container) // '3467i2fgz'

So in theory there could be other (custom) named ESM exports, unrelated to CSS Modules (locals) in particular

import * as styles from './file.css'

console.log(styles.meta)  // metadata...

One of the problems with this approach though is that it makes webpack trigger a second built every time the CSS is changed. I would instead like to be able to do the postcss processing through a loader, then export the results. I would also want to support CSS-modules in the long term, so I should be able to chain my loader after the css-loader, operate on the same raw css, then export both css-module specific data (class mapping) and the extracted query stats.

Currently I'm working on the long outstanding ESM upgrade of the CSS related loaders and a few things will likely change there, notably that CSS Modules will be removed from the css-loader and instead will be a opt-in via the postcss-loader and a separate plugin (postcss-modules) directly.

I have a container-query solution (https://github.com/ZeeCoder/container-query), which uses a postcss plugin to extract container-query-specific declarations and put them in a JSON / JS object.

Maybe using result.messages (PostCSS API) would be a better approach here

plugin.js

import postcss from 'postcss';

export default postcss.plugin('name', (options) => (css, result) => {
   // your plugin code here...
   result.messages.push({ 
     plugin: 'name', meta: { ...data } 
   })
});
// The postcss-loader handles result.messages (see comment below)
postcss([ plugin() ]).process(css, options)
  .then(({ css, map, messages }) => {
     console.log(messages) /* e.g [ { plugin: 'name',  meta: { ...data } ] */
  });

The postcss-loader will pass the messages (result.messages) as metadata (meta) to the css-loader (for CSS Modules atm) and the css-loader will add them as 'CSS Exports' like in the example above (previous comment). But an API for custom exports would definitely need triage and design :)

@ZeeCoder
Copy link
Author

Lots of interesting thoughts!

loader-exports

I like where things are going with this. But it's important we avoid potential name-collisions.
With the approach you described, the named "container" export is exported by the css-loader, which means it would export all classnames in the same fashion.
That would mean that other loaders now cannot use "container" and any other named exports that might be used as classnames.
Instead, loaders should probably be constrained to one named export, which is the loader's name, to avoid collision:

import { cssLoader as styles, otherLoader } from 'styles.css';
// Maybe without the "Loader" suffix? 🤔
import { css, other } from 'styles.css';

Where css.container is a hash (thanks to css loader being in module:true mode) and other contains whatever the other loader exported.

This way the following would be possible for my container query loader:
import { cs, containerQuery } from 'styles.css';

A thought:
Implementing something like this would mean that such loaders would pass an object to each other, containing the raw css as it's being processed, and the exports that each loader attaches.
In this case it would be possible to run a loader after the css-loader, which I would need to do anyway, since the css loader changes the classnames.
Also, what if style-loader is not the last one in the chain? Let's assume I don't want to include the CSS in the page, but might want to have custom exports regardless:

// with style-loader, css-loader and stats-loader chained together
import { css, stats } from 'styles.css';
// only with the stats-loader
import { stats } from '!!stats-loader?styles.css';

Basically, all loaders should work in both scenarios, which means styles-loader cannot be a mandatory loader at the end of the chain.

Is there any way I could help out in this effort?
Not sure who else would we need to talk to to make this discussion and the eventual implementation happen?

PS: I'm all for removing css modules from the css-loader, we'll just need to make sure then that these postcss messages can be / are exported.

postcss messages

I actually tried the :exports icss syntax, but it only allows for a single level, and the object I want to export is deeply nested.
(I actually might migrate the internals to use messages, but combined with the css-loader it still wouldn't work due to the above reason. Currently I use a callback because that's what the css-modules postcss plugin does too I think 🤔)
It would also not solve the other use-cases either, where exporting a JSON string is not sufficient and you want to export JS objects.

@ZeeCoder
Copy link
Author

To further demonstrate what I'm aiming for, I've made a prototype:
https://github.com/ZeeCoder/exporting-css-loaders

Hopefully it might kickstart further discussions?
@webpack-contrib-services @michael-ciniawsky

@ZeeCoder
Copy link
Author

@evilebottnawi , maybe? 😅
Not sure who to ping to have a discussion. 🤔

@michael-ciniawsky
Copy link
Member

@ZeeCoder Sry for the delay I didn't start to ignore this 😛, but I need to finish the exporting for CSS Modules (all related plugins with their result.messages) + style-loader@next + css-loader@next first to have something basically working (and maybe do an early alpha release for the loaders). I took a first glance at your example (which looks promising) and will take a deeper look when I'm ready with the CSS Modules stuff (~a few days) and come back here :)

@ZeeCoder
Copy link
Author

Sure, thanks for the update @michael-ciniawsky 👍

@michael-ciniawsky
Copy link
Member

0ee635d#diff-1fdf421c05c1140f6d71444ea2b27638R116

Usage

const plugin = 'my-plugin'

export default postcss.plugin(plugin, (options = {}) => (css, result) => {
     // Code ...     
     const data = // ...

     result.messages.push({
         type: 'export'
         plugin,
         export: `export const meta = ${data};\n` // Valid Named ESM Export {String}`
     })
})

webpack.config.js

{
   test: /\.css/,
   use: [
      'style-loader',
      'css-loader',
       {
          loader: 'postcss-loader',
          options: {
             plugins () {
                return [ require('my-plugin') ]
             }
          }
        }
   ]
}

file.js

import * as css from './file.css';

css.meta // ...data

@ZeeCoder
Copy link
Author

ZeeCoder commented Jan 8, 2018

I'll check it out as soon as I have some time, thanks for all the hard work. 🎉 👍

@michael-ciniawsky
Copy link
Member

Pending css-loader@next && style-loader@next release (1-2 days), so no hurry 😛

@ZeeCoder
Copy link
Author

I assume these were released? 😅

@ZeeCoder
Copy link
Author

I'm not too sure how to test this @michael-ciniawsky
I've tried cloning and building the next versions of both style- and css-loaders, but it doesn't seem to work with the current postcss-loader:

screenshot from 2018-01-28 13-29-36

👆 That's what I get with the postcss-loader added without any plugins.

@ZeeCoder
Copy link
Author

I'm getting the same error using postcss-loader#features/meta branch. 🤔

@ZeeCoder
Copy link
Author

ZeeCoder commented Feb 5, 2018

@michael-ciniawsky any idea? 🤔
I really want to try this out :)

@ZeeCoder
Copy link
Author

@michael-ciniawsky I'm still lost with this one, was there a release?
running yarn add css-loader@next gives me a list of available versions instead of installing. 🤔

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

No branches or pull requests

2 participants