facebook / stylex

StyleX is the styling system for ambitious user interfaces.
https://stylexjs.com
MIT License
8.24k stars 304 forks source link

[Enhancement] Improving the Webpack plugin? #297

Open nmn opened 6 months ago

nmn commented 6 months ago

Problem

The Webpack plugin and Next JS plugin have a bunch of issues at the moment. Thanks to help from Tobias Koppers, I have some ideas on how to fix these problems.

179, #197, #209, #288

Background

StyleX compilation is essentially two steps:

  1. Transform JS files and collect metadata (an array of objects)
  2. Take the metadata from all the JS files and convert them to a CSS file.

We haven't done this right within Webpack and caching doesn't work at all. But we now have some pointers on how to do this right.

Approach 1: Using CSS data imports

We can take the generated metadata from each JS file and convert that in the CSS data file import within the file itself.

Instead of generating:

["xabnbd", { "ltr": ".xabnbd{color: red}" }, 4001]

We would insert this import within the file itself:

import "data:text/css;@layer priority_4001{.xabnbd{color:red}}";

Then, the usual plugins will collect all the CSS from all the files combine them in a CSS file. (We need to ensure that there is only ever 1 CSS file. Figure out how to avoid bundle splitting for CSS)

This generated CSS file will then need to be processed with a custom PostCSS or LightningCSS plugin to do all the work that is currently done while generating the CSS file. (sorting rules, renaming layers. Removing unused styles etc.)

Challenges

  1. Need to ensure that no bundle splitting happens. We want a single CSS file
  2. We are planning to add new metadata that are not styles but information about which styles are defined and which ones are actually used. We will need to abuse CSS syntax to encode this into CSS itself.
  3. We will need some way to marking StyleX styles to distinguish them from non-stylex styles.
  4. All of the HACKs need to survive post-processing already done.

Possible solutions:

  1. We can use CSS layers with a special __stylex__ prefix and encode priority information in a suffix.
    • @layer __stylex__priority__3008 {...}
  2. Within such layers, we can use special classNames to mean other things:
    • .__stylex__VAR_USED {--variable-name: 1}
  3. We can configure Webpack to not split CSS with config that looks like this:
optimization: {
  splitChunks: {
    cacheGroups: {
      styles: {
        name: "styles",
        type: "css",
        chunks: "all",
        enforce: true,
      },
    },
  },
},

Approach 2: Write module metadata and combine it

This is the approach we use with the Rollup plugin today but we haven't done things in Webpack correctly.

The approach is similar to the first one, but here we can use a custom mime type to keep the metadata in JSON format.

We would insert an import into the source JS file that looks like this:

import "data:text/stylex,[\"xabnbd\", { \"ltr\": \".xabnbd{color: red}\" }, 4001]";

A webpack loader can be configured:

{mimetype: "text/stylex", use: "stylex-loader"}

This loader will need to read and attach the metadata to the module being processed:

this._module.buildInfo.stylex ??= [];
this._module.buildInfo.stylex.push(JSON.parse(content))

NOTE: the buildInfo is cached and needs to be serialisable.

Finally a Webpack plugin will need to iterate all the modules from compilation.modules to read the styles and combine them, generate a CSS file and use emitAsset to generate the CSS file.

In the case of HTML files being used, we would need to add the generated CSS to every entrypoint:

NextJS challenges

Even after all of this, there is a challenge with NextJS generating separate server and client bundles. The current approach handles this in a hacky unreliable way that can't be cached. Both of these approaches may be stuck with separate server and client CSS files which is not what we want. There was a mention of a "manifest" file as a possible way to get around this issue.

Workaround: Pre-compile everything

A third and final solution might be to build a CLI that can take a folder or "source" files and transforms them all along with a CSS file to a "src" folder. This would happen while you program rather than a part of bundling.

In more detail:

A pre-app or pre-src folder with your actual source code. A babel-watch command will transform this folder into app or src, respectively. Which runs in parallel with a standard swc-based next dev command combined with a single PostCSS or LightningCSS plugin.

Initially this won't support importing styles from node_modules, but we will be able to fix that with some import re-writing and configuration as well.

polywock commented 4 months ago

476 You can add this to the list of issues. The Webpack plugin is unusable for production because it interferes with Webpack's tree shaking/optimization. I'm using the Babel plugin as a substitute, but that's not meant for production.

SukkaW commented 1 month ago

@nmn FYI, here is currently my implementation (kinda like approach 1 and 2 combined)

import '/path/to/stylex-noop.css?rules=[serialized and encoded stylex rules]'

Actual implementation: stylex-webpack. This works with Next.js as well, since stylex-loader generated CSS imports can be handled by Next.js.

nmn commented 1 month ago

@SukkaW If your implementation works more reliably than the hacky plugin we have currently, I would love a PR to make it the official implementation going forward.

In the meantime, I'm going to do some testing with it and see how you got around some of the problems I encountered while trying to use that approach.

SukkaW commented 1 month ago

If your implementation works more reliably than the hacky plugin we have currently, I would love a PR to make it the official implementation going forward. In the meantime, I'm going to do some testing with it and see how you got around some of the problems I encountered while trying to use that approach.

Sure! IMHO we could do some experiments based on the stylex-webpack project before merging it back to stylex.