Skip to content

Commit e40cbaf

Browse files
authored
[feat] Add an unplugin bundler plugin implementation for various bundlers (#1300)
* [feat] Add new unplugin plugin * Add build scripts * Add RSPack example that uses unplugin * A imple Vite example * RSPack example and Vite with hot-reloading now working * fix bug with parsing errors on CSS files in Vite examples * Even works with Vite+RSC? * Everything works, but maybe code can be simplified * Fix simple vite example * Esbuild example updated to use unplugin * Add redwoodsdk example * Fix redwoodsdk for prod builds * even redwoodSDK is working minus hotreloading now * And hot reloading is now fixed * Bare minimum types for unplugin package * Refactor unplugin plugin a bit * Better location for filesystem style store * Fix small lint issues * Migrate webpack example to use unplugin * Update scripts and add README to examples * Add Waku example and update Readme with dev mode injection instructions * A fully working React Router with RSC example * Update readme for example-react-router * fix failing npm i * rebase * fix failing tests * add a test for the unplugin plugin
1 parent 937fdee commit e40cbaf

File tree

121 files changed

+29620
-16355
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+29620
-16355
lines changed

examples/example-esbuild/README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,60 @@
1-
# esbuild example using StyleX
1+
# StyleX with esbuild
22

3-
TBD
3+
This example bundles a React app with esbuild while compiling StyleX via
4+
`@stylexjs/unplugin`. The plugin extracts StyleX styles at build time,
5+
aggregates them, and appends the result to an existing CSS file produced by
6+
esbuild.
7+
8+
### Prerequisites
9+
10+
- Node.js 18+
11+
- [`esbuild`](https://esbuild.github.io/) (installed locally)
12+
- `@stylexjs/unplugin`
13+
14+
## Install dependencies
15+
16+
```bash
17+
npm install
18+
```
19+
20+
## Build script (`scripts/build.mjs`)
21+
22+
The build script wires the unplugin into esbuild:
23+
24+
```js
25+
import stylex from '@stylexjs/unplugin';
26+
27+
esbuild.build({
28+
entryPoints: ['src/App.jsx'],
29+
bundle: true,
30+
metafile: true,
31+
plugins: [
32+
stylex.esbuild({
33+
useCSSLayers: true,
34+
importSources: ['@stylexjs/stylex'],
35+
unstable_moduleResolution: { type: 'commonJS' },
36+
}),
37+
],
38+
});
39+
```
40+
41+
- `metafile: true` lets the plugin locate CSS assets emitted by esbuild.
42+
- `useCSSLayers: true` ensures the generated StyleX output is wrapped in CSS
43+
`@layer` declarations which enforces specificity. StyleX will use a polyfill
44+
based on ID selectors if omitted.
45+
46+
## CSS entry point (`src/global.css`)
47+
48+
The project imports `src/global.css` so esbuild emits a CSS asset. The StyleX
49+
plugin appends the aggregated styles to that file during
50+
`npm run example:build`.
51+
52+
## Commands
53+
54+
```bash
55+
# Production bundle + CSS extraction
56+
npm run example:build
57+
```
58+
59+
Use `npm run example:build` whenever you need a fresh `public/dist` folder
60+
containing both the JS bundle and the StyleX-enriched CSS.

examples/example-esbuild/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"private": true,
33
"name": "example-esbuild",
44
"version": "0.16.3",
5-
"description": "Simple esbuild example for @stylexjs/esbuild-plugin",
5+
"description": "Simple esbuild example for @stylexjs/unplugin",
66
"main": "src/App.jsx",
77
"scripts": {
88
"example:build": "node scripts/build.mjs",
@@ -15,7 +15,7 @@
1515
"react-dom": "^18.3.0"
1616
},
1717
"devDependencies": {
18-
"@stylexjs/esbuild-plugin": "0.11.1",
18+
"@stylexjs/unplugin": "0.16.3",
1919
"@stylexjs/eslint-plugin": "0.16.3",
2020
"esbuild": "^0.25.0",
2121
"eslint": "^8.57.1"

examples/example-esbuild/public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!doctype html>
22
<html>
33
<head>
4-
<title>@stylexjs/esbuild-plugin</title>
4+
<title>@stylexjs/unplugin (esbuild)</title>
55
<meta charset="utf-8" />
66
<style>
77
@layer reset {

examples/example-esbuild/scripts/build.mjs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,27 @@
88
import path from 'path';
99
import { fileURLToPath } from 'url';
1010
import esbuild from 'esbuild';
11-
import stylexPlugin from '@stylexjs/esbuild-plugin';
11+
import stylex from '@stylexjs/unplugin';
1212

1313
const __filename = fileURLToPath(import.meta.url);
1414
const __dirname = path.dirname(__filename);
1515

1616
const BUILD_DIR_NAME = 'public/dist';
1717
const OUTFILE = `${BUILD_DIR_NAME}/bundle.js`;
18-
const STYLEX_BUNDLE_PATH = path.resolve(
19-
__dirname,
20-
'..',
21-
`${BUILD_DIR_NAME}/stylex.css`,
22-
);
2318

2419
esbuild
2520
.build({
2621
entryPoints: [path.resolve(__dirname, '..', 'src/App.jsx')],
2722
bundle: true,
2823
outfile: OUTFILE,
2924
minify: true,
25+
metafile: true, // lets the plugin find CSS outputs, if any
3026
plugins: [
3127
// See all options in the babel plugin configuration docs:
3228
// https://stylexjs.com/docs/api/configuration/babel-plugin/
33-
stylexPlugin({
29+
stylex.esbuild({
3430
useCSSLayers: true,
35-
generatedCSSFileName: STYLEX_BUNDLE_PATH,
36-
stylexImports: ['@stylexjs/stylex'],
31+
importSources: ['@stylexjs/stylex'],
3732
unstable_moduleResolution: {
3833
type: 'commonJS',
3934
rootDir: path.resolve(__dirname, '../../..'),

examples/example-esbuild/src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
'use strict';
1111

12+
import './global.css';
1213
import * as React from 'react';
1314
import ReactDOM from 'react-dom';
1415
import * as stylex from '@stylexjs/stylex';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:root { --stylex-injection: 0; }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
node_modules
3+
.env
4+
dist
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# React Router + RSC + StyleX
2+
3+
⚠️ **EXPERIMENTAL**: This template demonstrates React Server Components with
4+
React Router.
5+
6+
## Highlights
7+
8+
- 🧪 React Server Components powered by Vite’s RSC plugin
9+
- 🧵 Styling via [`@stylexjs/stylex`](https://stylexjs.com/) compiled by
10+
[`@stylexjs/unplugin`](https://www.npmjs.com/package/@stylexjs/unplugin)
11+
- 🧭 React Router 7 data APIs + server actions
12+
- ⚡️ Instant HMR during `example:dev`
13+
- 🔣 TypeScript out of the box
14+
15+
## Scripts
16+
17+
```bash
18+
npm install # install dependencies
19+
npm run example:dev # start Vite dev server with RSC + StyleX
20+
rm -rf dist && npm run example:build # optional clean + production build
21+
npm run example:start # run the Express server using the dist/ output
22+
npm run example:typecheck # type-check without emitting files
23+
```
24+
25+
Dev server runs on `http://localhost:5173` by default.
26+
27+
## StyleX integration
28+
29+
The Vite config registers the StyleX unplugin before the RSC plugin so that both
30+
client and server bundles share the same compiled CSS:
31+
32+
```ts
33+
import stylex from '@stylexjs/unplugin';
34+
35+
export default defineConfig({
36+
plugins: [
37+
stylex.vite({ useCSSLayers: true }),
38+
react(),
39+
rsc({
40+
/* ... */
41+
}),
42+
],
43+
});
44+
```
45+
46+
- `src/stylex.css` is imported from the root route. This ensures Vite always
47+
emits a CSS asset that the unplugin can append to.
48+
- During development the layout injects the virtual StyleX runtime and
49+
stylesheet so HMR picks up CSS changes without reloading:
50+
```tsx
51+
{
52+
import.meta.env.DEV ? (
53+
<>
54+
<script type="module" src="/@id/virtual:stylex:runtime" />
55+
<link rel="stylesheet" href="/virtual:stylex.css" />
56+
</>
57+
) : null;
58+
}
59+
```
60+
- `@stylexjs/stylex` is used throughout the client components
61+
(`routes/root/client.tsx`, `routes/home/route.tsx`, etc.) instead of Tailwind
62+
classes.
63+
64+
## Hot reloading gotcha
65+
66+
React Router’s experimental RSC runtime exposes a `__reactRouterDataRouter`
67+
handle instead of the older `__router` object that Remix-based examples
68+
reference. To keep RSC + StyleX HMR working, the dev-only listener in
69+
`src/entry.browser.tsx` looks for either key before calling `revalidate()`:
70+
71+
```ts
72+
if (import.meta.hot) {
73+
import.meta.hot.on('rsc:update', () => {
74+
const reactDataRouter =
75+
(window as { __router?: DataRouter }).__router ??
76+
(window as { __reactRouterDataRouter?: DataRouter })
77+
.__reactRouterDataRouter;
78+
reactDataRouter?.revalidate();
79+
});
80+
}
81+
```
82+
83+
Without this fallback, RSC updates would silently no-op during dev.
84+
85+
## React Server Components recap
86+
87+
Three entry points orchestrate the RSC flow:
88+
89+
- **`entry.rsc.tsx`** — React Server request handler
90+
- **`entry.ssr.tsx`** — wraps the RSC payload in HTML for SSR
91+
- **`entry.browser.tsx`** — hydrates the RSC payload on the client
92+
93+
Routes are defined in `src/routes/config.ts` using `unstable_RSCRouteConfig`,
94+
and shared layout/UI lives in `src/routes/root`.
95+
96+
---
97+
98+
Built with ❤️ using React Router + StyleX.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "example-react-router",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"example:build": "vite build",
7+
"example:dev": "cross-env NODE_ENV=development vite",
8+
"example:start": "cross-env NODE_ENV=production node server.js",
9+
"example:typecheck": "tsc --noEmit"
10+
},
11+
"dependencies": {
12+
"@stylexjs/stylex": "0.16.3",
13+
"@remix-run/node-fetch-server": "0.8.0",
14+
"compression": "^1.8.0",
15+
"cross-env": "^7.0.3",
16+
"express": "^5.1.0",
17+
"react": "19.1.0",
18+
"react-dom": "19.1.0",
19+
"react-router": "7.9.2"
20+
},
21+
"devDependencies": {
22+
"@stylexjs/unplugin": "0.16.3",
23+
"@types/compression": "^1.8.1",
24+
"@types/express": "^5.0.3",
25+
"@types/node": "^24.0.3",
26+
"@types/react": "^19.1.8",
27+
"@types/react-dom": "^19.1.6",
28+
"@vitejs/plugin-react": "^4.5.2",
29+
"@vitejs/plugin-rsc": "^0.4.11",
30+
"typescript": "^5.8.3",
31+
"vite": "^6.3.5",
32+
"vite-plugin-devtools-json": "0.2.0"
33+
}
34+
}
14.7 KB
Binary file not shown.

0 commit comments

Comments
 (0)