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

How to extract StyleX CSS for Expo web? #34

Open
necolas opened this issue Feb 23, 2024 · 12 comments
Open

How to extract StyleX CSS for Expo web? #34

necolas opened this issue Feb 23, 2024 · 12 comments
Labels
bug: expo A bug that originates in Expo documentation Improvements or additions to documentation

Comments

@necolas
Copy link
Contributor

necolas commented Feb 23, 2024

Describe the feature request

The RSD example app uses Expo to target Android, iOS, and Web but does not support extracting styles to an external CSS file for web. For the purposes of the demo, we use Babel to process the styles on web but currently have to leave the runtime injection functions in the bundle.

The next step for improving Expo Web performance with StyleX would be to have a proper Metro integration that automatically extracts CSS and inserts the relevant <link> tag in the app shell. This could also help Expo become one of the easiest ways for React developers to replicate Meta's production setup on web, as other frameworks currently require developers to manually integrate and configure the relevant StyleX bundler plugin rather than working out-of-the-box.

(Opinionated authoring experiences, like Tailwind syntax for web and native, could then also be built on top of StyleX to avoid the need for Tailwind tooling and provide the robust style-merging mechanics of StyleX)

cc @EvanBacon @nmn

@necolas necolas added the documentation Improvements or additions to documentation label Feb 23, 2024
@nmn
Copy link
Contributor

nmn commented Feb 24, 2024

We would need a proper Metro plugin to handle this correctly. I'm not sure what that process looks like, but in the meantime, I'm working on two stop-gap solutions:

  1. Using CSS data url imports and a PostCSS plugin the process the generated CSS file. I'm not sure if Metro supports CSS imports?
  2. Joel and I are also working on a CLI to compile a folder that could be used as an escape hatch when a bundler plugin is not ready.

@necolas
Copy link
Contributor Author

necolas commented Feb 26, 2024

@motiz88 Do you have any insight into how best to create a Metro plugin for StyleX? (Similar to the ones we have for other bundlers)

@javascripter
Copy link
Contributor

javascripter commented Apr 11, 2024

Not too sure if this is the right approach but here's my attempt at css extraction for Expo Web if it helps.
https://github.com/javascripter/stylex-expo/

@necolas
Copy link
Contributor Author

necolas commented Apr 11, 2024

Thanks for giving this a go and including a write up! I might be interested in incorporating some version of this into the example app if it would help Expo provide something for StyleX out of the box soon. But we haven't had any input from Expo yet.

Since Expo Web isn't really used for web-only apps, I might add a something else (like a Next app) so developers can see RSD working in a production setup with tooling they already know. I want to be able to highlight that RSD is also good option for web-only experiences that want to adopt StyleX and want to avoid exposing deprecated/legacy DOM APIs to their development team.

@javascripter
Copy link
Contributor

javascripter commented Apr 12, 2024

Yes, please feel free to use my code as a starting point if needed. I wrote it from scratch but it's worth mentioning some ideas came from this stylex webpack plugin.

I understand the point about having a Next.js example as well.
Expo for Web depends both on react-native-web and react-navigation on the web now so it can be a good compromise for universal apps but for web-only experiences Next.js can be a better fit.

Having one Expo example to showcase universal usage and another Next.js example for web usage should satisfy most users I believe.

@bayareaunicorn
Copy link

I understand the point about having a Next.js example as well. Expo for Web depends both on react-native-web and react-navigation on the web now so it can be a good compromise for universal apps but for web-only experiences Next.js can be a better fit.

Having one Expo example to showcase universal usage and another Next.js example for web usage should satisfy most users I believe.

While this maybe true I think a platform agnostic package tied away from Expo is what anyone with any serious grease needs.

@necolas
Copy link
Contributor Author

necolas commented Apr 12, 2024

If you're not using Expo/Metro (on web) then you don't need a Metro plugin for StyleX. RSD just works on bare React Native apps - that's what we use internally. The only people seriously using Metro on web today are Expo users. If anyone is trying to use RSD with Expo to target web, you might have more luck opening issues against Expo. Or switch Expo over to using webpack and integrate the StyleX webpack plugin

@necolas necolas changed the title How to extract StyleX CSS for Expo web How to extract StyleX CSS for Expo web? Apr 18, 2024
@yusufyildirim
Copy link

yusufyildirim commented May 6, 2024

I wrote something very crude based on the official Webpack plugin. Sharing here just in case it helps somebody.

Create metro-stylex-transformer.js:

/* eslint-disable @typescript-eslint/no-var-requires */
const upstreamTransformer = require('@expo/metro-config/babel-transformer');
const svgTransformer = require('react-native-svg-transformer');
const path = require('path');
const babel = require('@babel/core');
const stylexBabelPlugin = require('@stylexjs/babel-plugin');
const fs = require('fs');

const importSources = [
  '@stylexjs/stylex',
  'react-strict-dom',
  // This should be identical to the `importSources` you provide to StyleX plugin in your Babel config
];

// Whether you're okay with layers or not (falls back to `:not(#\#)` when false)
const useCSSLayers = true;
const stylexRules = {};

const getStyleXRules = () => {
  if (Object.keys(stylexRules).length === 0) {
    return null;
  }

  const allRules = Object.keys(stylexRules)
    .map((filename) => stylexRules[filename])
    .flat();
  return stylexBabelPlugin.processStylexRules(allRules, useCSSLayers);
};

/**
 * It's a very crude implementation for creating a CSS file out of StyleX styles on the build time.
 * It has to be run on a single worker with this implementation to prevent overwriting the CSS file.
 * Should only run for the release builds.
 *
 * Implemented based on the official Webpack plugin.
 * See: https://github.com/facebook/stylex/blob/main/packages/webpack-plugin/src/index.js
 */
module.exports.transform = async ({ src, filename, options }) => {
  if (importSources.some((source) => src.includes(source))) {
    const res = upstreamTransformer.transform({ src, filename, options });
    const { metadata } = res;

    if (metadata.stylex != null && metadata.stylex.length > 0) {
      stylexRules[filename] = metadata.stylex;
      const style = getStyleXRules();
      fs.writeFileSync('dist/stylex.css', style);
    }
    return res;
  }

  // just replace `svgTransformer` with `upstreamTransformer` if you don't need it.
  return svgTransformer.transform({ src, filename, options });
};

Create an empty stylex.css file next to your index.html

Add this to head in your index.html:

    <link rel="preload" href="./stylex.css" as="style" />
    <link rel="stylesheet" href="./stylex.css" />

Change your metro.config.js

// Determine you're exporting somehow. Reading an env var should do it.
// I'm sure Expo is setting something automatically but I didn't bother looking into it.
const isExporting = process.env.EXPORTING; 

const config = {
  // ...
  transformer: {
    // You don't want stylex transformer to work on dev at all
    babelTransformerPath: isExporting
      ? require.resolve('./metro-stylex-transformer')
      : require.resolve('react-native-svg-transformer'),
  },
  // ...
 }

Use it: EXPORTING=true expo export -p web -c --max-workers 1

We're doing something outside Expo's asset resolution/handling here. We directly load the CSS inside the html so Expo doesn't rename it to something obscure etc. We created an empty CSS file which expo will copy to the dist file alongside index.html. Our transformer will populate that file on the build time.

The downside of this approach is that, it works on a single file synchronously. That means we can't allow any parallelism, every instance will override the file otherwise. That's why you have to set maxWorkers: 1 in your metro config or provide --max-worker 1 with your Expo build command.

I had very limited time (a couple of hours) and this is what I came up with. I'll probably improve it whenever I find some time but ideally, this should be a part of Expo's asset process.

EDIT: Realized that I somehow missed the other implementation shared above 😄

@nmn
Copy link
Contributor

nmn commented May 8, 2024

fs.writeFileSync('dist/stylex.css', style);

It looks like this will overwrite the CSS file for every JS file that is transformed. This will work as the styles are style being collected, but ideally it should be done once.

A slightly better hack might be to use a throttled function that only runs once JS files are done transforming.

@motiz88
Copy link

motiz88 commented May 8, 2024

I would strongly recommend against using a side-effectful Metro transformer (it breaks pretty fundamental assumptions in Metro re: caching), and I'd ask that we don't recommend such an approach officially.

There isn't a plugin system as such in Metro, but there are plenty of existing extension points, and we can always refine them or expose new ones as needed to support StyleX / RSD in Metro projects. Happy to give pointers to anyone interested in hacking on this.

(cc @robhogan for visibility)

@yusufyildirim
Copy link

Thanks! It's good to note that this is a temporary hack and I explicitly pass -c flag to avoid any caching related potential issue for that reason.

@javascripter
Copy link
Contributor

javascripter commented Oct 23, 2024

I’ve developed another integration of RSD/StyleX with Expo Web using a custom PostCSS plugin.
It is inspired by StyleX CLI & Panda CSS and sidesteps Metro integration by separating babel.config.js for JS compilation and postcss.config.js for CSS extraction. Next.js and Turbopack can be supported as well because it's bundler-agnostic.

It supports Expo Web and works with upcoming universal RSC and Expo DOM Components features as well.

I believe Babel + PostCSS Plugin is an overall more robust approach because it has built-in file dependency tracking for watch mode and does not have Metro cache invalidation issues. PostCSS is already deeply integrated with frameworks like Expo to allow for Fast Refresh. (It would be better if we have a proper official integration that doesn't rely on separate process though).

I published a working PoC with explanation in the README here:
expo-stylex-postcss-integration

Feedback is appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug: expo A bug that originates in Expo documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

6 participants